Found by phugoid rung 4 (the N-aircraft swarm), 2026-08-26, eigenscript v0.41.0.
This body has been rewritten twice. The original claimed a runtime-sized population "cannot be observed per-aircraft at all"; the first correction weakened that to "only a lexical name, so it forces eval-generated source". Both were false — each was a negative claim published after a non-exhaustive search. eval works, and so do closures. The accurate statement is below, and it is a claim about the mechanism rather than an enumeration of what I happened to try. See the correction comments for the history.
The mechanism
Observer trajectory is keyed to an environment slot — env_obs_slot(Env *e, int idx) returns e->obs[idx] (src/eigenscript.h:1352), and the header says so directly at line 346: "The Value carries no observer state."
So a per-entity observer channel requires a persistent environment slot per entity. That is the whole rule, and it predicts every case:
| form |
persistent env slot? |
trajectory |
| distinct named locals |
yes |
works |
| closure captured local (one factory call per entity) |
yes |
works, N chosen at runtime |
eval-generated names |
yes |
works, N chosen at runtime |
dict field ch.a |
no — a Value in a container |
equilibrium, no history |
list element xs[0] |
no — a Value in a container |
equilibrium, no history |
| function parameter |
slot exists but the frame dies each call |
equilibrium, no history |
Closures, verified with N from argv:
define make_ch as:
local q is 0.0
define step(v) as:
q is v
return report of q
return step
$ eigenscript g8.eigs 6
closure channels (n=6 from argv): ["moving","oscillating","moving","oscillating","moving","oscillating"]
Alternating decaying/oscillating trajectories, correct per-channel verdicts, ordinary static source that lints normally.
What the actual gap is
Containers cannot carry observer state, so per-entity observation cannot be expressed the way a consumer would naturally reach for it — a fleet is a list, a registry is a dict, and neither can be observed element-wise. The available forms all require one binding per entity: a named local, or a closures captured local.
That is a real ergonomic gap and it has a sharp failure mode: the obvious loop
loop while i < n:
local q is fleet[i][2] # ONE binding, rebound N times
if diverging of q: ...
does not merely lose resolution, it manufactures verdicts — the window becomes the round-robin interleave, so a monotonically decaying trajectory reads oscillating:
shared binding : a-verdict=oscillating b-verdict=oscillating
named bindings : a-verdict=moving b-verdict=oscillating
That is silent-wrong with no diagnostic, and it is what rung 4s own first draft shipped in every arm.
The ask, unchanged and now correctly weighted
Make dict-field and list-element assignment carry trajectory keyed by (container identity, key). With closures available this is convenience rather than necessity — but it is the form consumers reach for first, and the alternative failure is silent rather than loud.
A secondary and cheaper mitigation: make the interleaved-binding case detectable. A binding whose observed values jump between unrelated trajectories is a bug in every case I can construct, and the observer has the history to notice.
Found by phugoid rung 4 (the N-aircraft swarm), 2026-08-26,
eigenscriptv0.41.0.The mechanism
Observer trajectory is keyed to an environment slot —
env_obs_slot(Env *e, int idx)returnse->obs[idx](src/eigenscript.h:1352), and the header says so directly at line 346: "The Value carries no observer state."So a per-entity observer channel requires a persistent environment slot per entity. That is the whole rule, and it predicts every case:
eval-generated namesch.aequilibrium, no historyxs[0]equilibrium, no historyequilibrium, no historyClosures, verified with N from argv:
Alternating decaying/oscillating trajectories, correct per-channel verdicts, ordinary static source that lints normally.
What the actual gap is
Containers cannot carry observer state, so per-entity observation cannot be expressed the way a consumer would naturally reach for it — a fleet is a list, a registry is a dict, and neither can be observed element-wise. The available forms all require one binding per entity: a named local, or a closures captured local.
That is a real ergonomic gap and it has a sharp failure mode: the obvious loop
does not merely lose resolution, it manufactures verdicts — the window becomes the round-robin interleave, so a monotonically decaying trajectory reads
oscillating:That is silent-wrong with no diagnostic, and it is what rung 4s own first draft shipped in every arm.
The ask, unchanged and now correctly weighted
Make dict-field and list-element assignment carry trajectory keyed by (container identity, key). With closures available this is convenience rather than necessity — but it is the form consumers reach for first, and the alternative failure is silent rather than loud.
A secondary and cheaper mitigation: make the interleaved-binding case detectable. A binding whose observed values jump between unrelated trajectories is a bug in every case I can construct, and the observer has the history to notice.