Engineering Notes and Cookbook: the qwen38 conversion resumes
Engineering Notes gets the full record: what the resume path adopts and refuses
(whole shards only, size-checked against the header's declared payload), the
invariants the end-to-end suite forced out (compacting survivors after a
discarded shard, dropping tensors already in the index, the conversion.json
width marker), the n-gram table's in-place reuse and cross-filesystem copy, the
download retry loop and its curl-33 restart, `--endpoint`/HF_ENDPOINT mirrors on
the Hub layout, and what the 36-case fault-injection suite does and does not
establish.
The Cookbook's install recipe gains the user-facing half: the conversion resumes
at the shard level, HF_ENDPOINT points it at a mirror, a run stopped before the
index write refuses rather than duplicating a 360 GB snapshot, and no mirror's
bytes are hash-checked.
Engineering Notes: the native-build script is gone, the finding stays
Naming this Mac's CPU measured about 1% on the CPU int8 GEMV -- noise -- so
tools/build-native.sh was removed as one more surface claiming an optimization it
did not deliver; what improves the build is the C -O2, which is a package default.
The flag combination itself is kept in the notes for anyone who wants a
non-portable build on their own machine.
Engineering Notes: the -O2 re-record, and the --ram targets at a medium prompt
The re-record on the idle machine (load 2.5) settles the CPU-GEMV question: 50.6
GB/s now against 51.4 recorded, -1.6% and inside the record's own scatter, so the
45.7 the first -O2 build showed was the machine at load 5-6. Decode +2.5%, prefill
+6.5%, TTFT -6.1%, quality unchanged; gpu.routed_moe's +46% is not claimed because
this file already records that counter as schedule-sensitive. Runtime-Controls
gains the medium-prompt verification of the process target: --ram 8G is 32 slots /
7.87 GiB peak, --ram 10G is 48 slots / 9.86 GiB, both identical round to round,
0.2 GiB above their estimate, no swap growth, byte-identical answers.
Engineering Notes: Metal needs no M3 compiler flag
Shaders are compiled at runtime on the device, so they are already generated for
the GPU in the machine -- there is no apple-m1-baseline analogue on the GPU side,
and the runtime options are already at their fast settings. Measured with an
inf * 0 discriminator: with languageVersion 4.0 and mathMode unset the result is
0.0 (MTLMathModeFast behaviour) where .safe and .relaxed keep nan;
optimizationLevel offers only Default and Size, so there is no runtime -O3.
Precompiling would save the cold compile alone (0.86 s for the 15-module library,
0.00 s afterwards from the driver cache). Real M3 GPU gains are kernel work:
apple9 is true and apple10 false here, so the tensor-op prefill path stays gated
off, and bfloat16_t, simdgroup_matrix and simdgroup_multiply_accumulate are the
available tools.
Engineering Notes: -O2 is the project default for the C kernels
The previous entry recorded the 1.24x the C optimization level is worth and left
it as a decision; Package.swift now sets .unsafeFlags(["-O2"]) on
TinyTitanKernelsC, so the portable build carries it and tools/build-native.sh only
adds the CPU selection. Measured default against the previous -Os artifact:
2.35 -> 1.94 ms per pass on the CPU int8 GEMV, min of six interleaved rounds,
1.21x, checksum 390266.62 both. The cost is that .unsafeFlags stops the package
being usable as a dependency, acceptable because it is an application package with
no dependents.
Engineering Notes: the release build targets apple-m1, and C at -Os
Records the compiler reality behind the new tools/build-native.sh: no -mcpu means
clang's default CPU for arm64-apple-macos*, which is apple-m1, so an M3's BF16 and
I8MM go unused; and SwiftPM's swiftbuild system compiles every C target at -Os
where the older native planner used -O2. Measured on the M3, interleaved, six
rounds an arm, CPU int8 GEMV at 8 threads with an identical checksum: 2.06 min /
2.40 median ms per pass portable, 1.66 / 1.73 at -O2 on the M1 baseline (1.24x),
1.64 / 1.69 at -O2 plus apple-m3 (1.26x). The win is the C optimization level,
not the CPU selection. Making -O2 the default would need .unsafeFlags, which
would stop the package being usable as a dependency -- the same reason -O3 was
rejected in Package.swift.
Notes: the GDN a/b validator refused the slot's own width (issue #16)
Recorded with the reproduction (the override added to a qwen38flash manifest,
receipt updated so the load is not refused for the edit), the fix (the check now
takes attentionBits and accepts the slot width or bf16), and the lesson: a rule
that names a slot has to be given that slot, and naming the slot's width is a
description rather than an override needing honour.
Tracker round 32: TT-025's closure made canonical
TT-025 is now a first-class entry under "Closed by measurement -- do not
re-propose", not a sentence in that section's intro: the conversion-time
question (which precision-sensitive tensors to keep at 8 bits rather than 4),
the plan that named k_proj/v_proj at 16 MB, the two end-to-end results that
close it (18/20 vs 18/20, and -0.009727 +/- 0.006665 nats, t -1.46), what was
therefore not built, and an explicit do-not-revive-without-a-125B-machine note.
The first check's "what it does not establish" now points at the sharper
perplexity check instead of saying it was never run, which was the last
sentence that could read as if the task were still open.
Tracker round 31: TT-025 closed by measurement
Both quality instruments now agree. The twenty-prompt checkable suite is 18/20
against 18/20 for the promotion and its --no-promote control, and the paired
held-out perplexity A/B puts the 16 MB promotion 0.0097 +/- 0.0067 nats ahead
(t -1.46) and the whole-slot 8-bit build 0.0012 nats ahead (t -0.09) over 1,023
positions -- in precision's favour and below the instrument's ~0.013-nat
floor. The runtime half ships and is tested; the converter policy and the GDN
a/b kernel's int8 branch are not built, and the Notes now say so where someone
looking for a closed quantisation idea will find it.
The Open table is TT-018 alone, TT-020 is the only blocked item, and Parked is
empty.
Tracker round 30: TT-033's caller lands, TT-025's sharper check finds the same floor
TT-033: `MemoryRetrievalHinter` is T7's caller -- a background sweep that runs
only in the idle window and leaves a ranking hint for a later search, never a
request. The Notes record the design, its three bounds (64 facts a question,
16 questions tracked, a value fingerprint against staleness) and the seven
behaviours the unit suite pins. The row is closed and deleted from Parked,
which is now empty.
TT-025: `TinyTitanBench cpu35ppl` scores a fixed held-out text through the CPU
forward pass, and the paired comparison over 1,023 positions gives the 16 MB
k_proj/v_proj promotion 0.0097 +/- 0.0067 nats ahead of its --no-promote
control (t -1.46) and the whole-slot 8-bit build 0.0012 nats ahead (t -0.09).
Both point estimates favour precision and neither clears the instrument's
~0.013-nat floor, so the task stays open with that recorded; the Notes also say
what a paired t over correlated positions does not prove.
Also in this round: TT-009's side-engine model policy is written down (2B
unused, 4B 4-bit the default and verification instrument, 9B optional on
quality benchmark results), and TT-021, TT-022 and TT-023 are closed -- no
other machines for chip validation or ANE across generations, and no disk for
the ~360 GB bf16 reference long-context parity needs -- with the caveats they
existed for left standing in Blocked work.
Tracker round 29: TT-025's quality case is measured, and it is not there
The measured question was whether a per-tensor promotion inside a 4-bit install
buys anything end to end. On a twenty-prompt checkable suite: the 4B's own 16 MB
`k_proj`/`v_proj` promotion is 18/20 against its uniform control's 18/20 with
identical outputs; 4-bit vs 8-bit is 18/20 vs 17/20 on the 4B and 18/20 vs 18/20
on the 9B. The failures are reasoning, not precision.
The mechanism still ships (the runtime half), and the Engineering Notes entry
records the instrument, the controls and what a wash at this resolution does not
prove. What is left of the task has no measured justification, and the 125B that
would test the slot-reading tensors needs a 360 GB bf16 source this machine
cannot hold.
Tracker round 25: TT-024 closed — CPU co-execution is rejected by measurement
The parked row still argued from the idle GPU ("occupancy 39.5-48.5%, 53% of it
inter-command-buffer gaps"), but the plan document it cites reaches the opposite
conclusion two thirds of the way down: running the real CPU expert kernel as a
load generator during decode raises GPU-busy 44.9% (27.3 -> 39.6 ms) and costs
22.6% of throughput. At best the CPU absorbs 32% of the work and the GPU does
the rest at 0.69x, so no split ratio wins. The plan's own words are "dead, not
deferred". A streaming-read probe had supported the idea and was wrong about the
workload. The rejection and its numbers are now an Engineering Notes entry under
"Closed by measurement — do not re-propose", and the kernel stays in the tree
with the note that nothing may call it from the decode path.
TT-025 was checked the same way and is accurate, so it keeps its place — with the
evidence added: `prepare_qwen38.py`'s `quant_bits` documents the runtime
rejecting a per-tensor override ("in_proj_a.weight size 122880 does not match
expected 61440"), which is exactly the slot model the row describes.
Parked now holds TT-025 and TT-033; three of the five rows that were there at
the start of this audit (TT-024, TT-026, TT-027) are closed.
Tracker round 18: TT-003 closed — the unrequested thought is prompt-dependent, not width-dependent
The restored 8-bit install (golden-verified) let the single-prompt observation be
re-run across prompts, and it does not survive. At `--reasoning off`, greedy, 128-token
cap, both widths:
Capital of Paris 8-bit 477 reasoning (empty answer) 4-bit 0 (answered)
What is the capital of Paris? 8-bit 470 4-bit 471
Who is the president of Paris? 8-bit 0 4-bit 0
How many capitals does Paris have? 8-bit 487 4-bit 504
What is the capital of France? 8-bit 0 4-bit 0
Name the capital city of Japan. 8-bit 0 4-bit 0
Both widths reopen a closed `<think>` on different false-premise prompts — the 4-bit
thinks on two of the four, and not on the one the original row used — so this is a
property of the prompt and the family's weak closed-block cue, not of the 8-bit width.
`--reasoning on` changes the thought's content rather than deciding whether there is
one, so the level is a request, not a gate.
Catalogue decision: advertise nothing differently. `off` describes what the server
asks for, and it does ask; special-casing one width would encode a single-prompt
artefact, and dropping `off` from the family would be wrong because it does not gate
the behaviour in either width. C90's split and its `thinking off, but the model wrote
N characters` log line (which fired on all five thinking runs) are the honest surface.
Engineering Notes carries the matrix and the decision; the Capital-of-Paris page's
"its 4-bit sibling never does it" is corrected there too.
Tracker round 17: TT-010 closed by measurement — the QSA-on-GPU ceiling is 2.75%
The prefill QSA selection is a host computation behind a barrier, and the decode half
of the same idea already shipped as an opt-in and measured a wash. `TINYTITAN_QSA_SELECT_TRACE=1`
now times the prefill selection per layer per chunk on qwen3.8-flash-next 4-bit:
prompt 2,410 tok prefill 95.64 s selection 185.7 ms -> 0.19%
prompt 11,948 tok prefill 864.90 s selection 23,748.7 ms -> 2.75%
It is superlinear (first chunk 1,775 ms, second 9,222, third 12,752) because rows past
the ~2,051-key exactness window pay a block sort plus an O(visible) compaction. Both
runs shared the machine with a model conversion, so those are upper bounds.
Rejected on the ceiling: a GPU prefill path needs a multi-row mask kernel and GPU
compaction the decode path does not have, could recover at most 2.75% on one family's
longest prompts, and any tie-break difference changes which keys are attended. The
trace switch stays so the number can be re-taken. Recorded under "Closed by
measurement — do not re-propose"; TT-010 removed from Open.
Tracker round 16: TT-004 closed — the E5RT arenas are returned, and TT-005's prompt was too short
TT-004's candidate is dead. With a new `TINYTITAN_ANE_MEMORY_TRACE=1` on the installed
pair and an 11,948-token prompt (two full sidecar chunks), the arena measures ~152 MiB
and `releaseModels()` returns it synchronously — the footprint falls by exactly that
at `residentModel = nil`. At the prefill→decode boundary the ANE arm sits −239 to
+318 MiB from the GPU arm, so nothing arena-sized survives; the ~1 GB the hypothesis
needed is not there. The 3.6x longer expert-read awaits (859 ms against 3,129 ms on
identical bytes) therefore have no accepted cause.
The same runs exposed a flaw in TT-005, corrected in Engineering Notes: its
4,043-token prompt is one partial chunk, below the sidecar's exact-4096 floor, so both
"ANE" arms fell back to the GPU (`ane-prefill fallback: chunk at 0 (+4043) outside
sidecar coverage`). The wiring result stands — it is cache state, not engine state —
but it was not measured after an ANE prefill. Re-run on the eligible prompt, the
pageable cache re-pins all 40 layers in 10.78 s at the first decode token against
13.4 ms pinned, which is the window penalty TT-005 identified, now measured with the
sidecar engaged.
TT-004 removed from the tracker; the ANE decode-gap observation now says its fourth
candidate is dead rather than untested.
Tracker round 15: TT-002 closed — the 2B's two answers are one near-tie, measured
A new `TINYTITAN_LOGIT_TRACE=1` on both sampling paths prints each step's top-2 and
margin, which the equivalence gate never did. On the same chat request through one
server: the engines agree for four generated tokens and diverge at the fifth, where
the GPU ranks `,` 24.67 over ` is` 24.27 (margin 0.41) and the CPU ranks ` is` 25.71
over `,` 24.30 (margin 1.40) — the same two candidates, swapped, with the engines'
logits for the same token 1.44 and 0.37 apart.
How close a tie has to be: over the first 17 steps' shared top-2 candidates the
cross-engine |logit difference| is min 0.007, median 0.90, max 4.02, so a step flips
whenever its margin falls under that spread — step 4 at 0.41 did, steps 11 and 34 at
0.45 and 0.16 did not, the error favouring the same token. Neither engine is wrong;
the 4-bit weights make the comma continuation nearly equiprobable, which is a quality
finding, not an engine defect. A third opinion needs the numpy reference and its
qwen35-2b snapshot, which is absent.
Engineering Notes carries the table and the consequence; the "Unexplained
observations" entry now points at it.
Tracker round 14: TT-003 moved to Blocked — the 8-bit install it needs is not here
What the task asked is now split into what is established and what is not.
Established, from the installed tree: the 4-bit template renders `enable_thinking:
false` as a **closed** block (`tokenizer/chat_template.jinja:154`), so the model sees
`</think>` and opens a second one; the two installs' templates were checked
byte-identical when both existed; and the request path is already handled by C90,
which routes an unrequested thought to `reasoning_content` and logs its size. The
width disagreement is therefore in the weights, and since 8-bit is closer to bf16
than 4-bit, the prediction to test is that the reference reopens the block too and
the 4-bit answer is the outlier.
Not established, and not establishable here: AgentWorld 8-bit is absent from
`models/`, was never part of the verification set, and fetching, converting or
re-installing one to satisfy a task is forbidden. Neither its weights, template or
manifest, nor a bf16 reference, is on this machine. The catalogue decision —
whether an install whose weights ignore the closed-block cue should still advertise
`off` — is generated per install and cannot be made for hardware this repository
cannot test without it becoming an assurance.
Moved to the Blocked table with that condition. Engineering Notes carries the
evidence, what is ruled out, and the prediction. The goal stays active; the operator
can clear this by restoring the install.
Tracker round 13: TT-005 closed — the ANE window penalty is a re-warm, and the allocation-time pin removes it
The three candidates for the residual −14.7% are separated by the wire trace rather
than by rates. AgentWorld 35B-A3B 4-bit, 4,043-token prompt, two runs per arm:
KEEP_WIRED=1 (shipped, pinned at allocation): no unpin at prefill, no re-pin at the
first decode token — a re-warm is impossible because nothing was ever evicted
KEEP_WIRED=0 (pageable): the first decode token re-pins for 888 / 1,588 / 2,084 /
22,077 ms, i.e. 13-24% of a 64-token window, and longer than the whole window
when memory pressure hits
That magnitude and spread is the pre-pin and phase-scoped window figures (−27.9%,
−53.6%), so the window penalty is the pageable wiring's re-warm. Steady state is clean
either way (pinned ANE vs GPU −1.1%), reproducing TT-007, so a surviving arena
footprint is below the machine's spread and belongs to TT-004. What is left is drift,
already retracted in v4.6. Engineering Notes carries the table, the verdict, and the
session caveat (browser CPU/~19% GPU load; prefill spread 96-162 s on one identical
configuration) — the wire times are direct, the pageable rates are not quoted.
Tracker round 12: TT-006 closed — the MTP verify cost is attributed, and the union was never the problem
Re-done on the install that exists (Qwen3.8-Flash-Next 4-bit + its draft head), with
the phase harness generalized off the uninstalled Ornith map. 256 greedy tokens, 86.9%
acceptance, 1.869 emitted per pass, two runs per arm agreeing to 0.05%:
verify backbone (wall) 417.35 ms = 1.946x a 214.45 ms scalar token
routed pair (union) 51.9 ms = 0.242x -> 1.45x the scalar routed GPU, at or
under the 1.585x union model
non-expert prefill 165.3 ms = 0.771x -> 1.70x, where the model assumes 1.0x
host + commit + wait 200.2 ms = 0.933x -> entirely unmodelled
pass total 480.03 ms = 2.238x (break-even needs < 1.869x)
The union is at the model's price, so the 0.4-0.7x is the prefill path: two rows
through the 32-token prefill kernels rather than the decode ones, plus ~200 ms/pass of
per-tile argument buffers, command buffers, planning and sequential fetch awaits.
Acceptance is therefore not the lever — 86.9% still decodes 16.5% slower than scalar
with byte-identical output — which refutes the older reading; the stale doc comment
saying otherwise is corrected in the checkout. Engineering Notes carries the table and
the caveats (browser CPU contention, GPU 3% busy; the host figure also matches v4.4 B1
on a different model).
Tracker round 11: TT-011 closed — the 128-slot budget is real, and the machine refuses it
The proposal rested on a simulated LRU trace (65% hit at 64 slots, 78% at 128 "where
it saturates"). Sweeping the runtime's own count, interleaved, on a 7,550-token prompt
with 256 new tokens, two runs per point:
64 slots 7.92 GiB 67.7% hit 3.44 tok/s 408.0 MiB/token
96 slots 11.88 GiB 76.5% hit 3.66 tok/s 296.7 MiB/token (shipped)
128 slots 15.84 GiB 82.6% hit 1.21 tok/s 220.0 MiB/token
The hit-rate case for 128 is real and the simulated saturation is wrong — it gains 6.1
points over 96 and cuts expert reads 26% — but decode falls 3x, reproducibly, because
15.84 GiB plus the mapped weights no longer fit a 24 GiB Mac. 64 slots is cheaper and
slower, so 96 stays the default, which the half-of-RAM affordability cap already
encodes; 128 is reachable with `--ram-budget 16G` where memory allows. No code change.
Engineering Notes carries the curve and the reasoning; the port document no longer
claims the trace's saturation.
Tracker round 10: TT-007 closed — steady-state decode after ANE prefill costs nothing
Every ANE decode figure here was a ~60-token window, because the pinned
qualification prompt hits end-of-turn and `--max-new` never bound; the window
contains the one-time handover cost, so those figures were upper bounds and the
steady-state cost had never been measured.
The prompt fix — the qualification body plus "continue, do not stop early" — makes
the cap bind. `benchmark/ane_steady_state_decode.py` then differences two generation
lengths so every one-time cost cancels. On AgentWorld 35B-A3B 4-bit, an 11,948-token
prompt, two runs per arm and length: ANE prefill is 3.32x faster and costs **+0.7%**
of steady-state decode (7.26 against 7.21 tok/s) — nothing, inside the spread — with
no penalty even in the 64-token windows.
Engineering Notes records the method, the numbers, the caveats (medians of 2; the GPU
arm's decode follows a 10-minute GPU prefill against ANE's 3-minute one; Ornith is not
installed, so AgentWorld-4 stands in; its row wires the cache, so the pageable case is
TT-005's question via the new `TINYTITAN_KEEP_WIRED=0`), and a new trap: the first ANE
prefill of a process pays a 2.1x compile warm-up, so a single cold-process figure
overstates the sidecar. Benchmarks carries the table.
Tracker round 9: TT-001 closed — the ThreadSanitizer report is a toolchain false positive, suppressed at the top frame
Reproduced by running six instrumented server bundles at once (0-7 reports per 12 runs;
a single process is almost always clean). The report is always an 8-byte write in
`SSEOutbox.next()` against an 8-byte read in `UnsafeContinuation.resume` from NIO's
`EventLoopFuture.get()`, on the drainer task's own heap allocation — not the outbox,
whose fields are all lock-guarded and whose four resume sites have no double-resume
path. It is a `swift_task_switch` happens-before that TSan does not model when the
resumer is not a Swift task, and contention is what exposes it.
Closed with `race_top:UnsafeContinuation.resume` in `tools/tsan-suppressions.txt`, wired
into the CI job, with `tools/tsan-storm.sh` kept as the reproduction that re-justifies
it (`--no-suppressions` for the raw report). The suppression is top-frame scoped, so a
real race in TinyTitan code still fails the gate. Engineering Notes carries the report,
the reasoning and the measurements.
Tracker round 8: TT-008 closed — the expert-cache wiring honours =0
`TINYTITAN_KEEP_WIRED` was read as `== "1"` in three places, so it could only turn the
cache on, and every 35B row already wires it: on a 24 GB Mac the 12 GiB cache could
not be paged out at all and `=0` was a no-op on exactly the installs that need it. It
is a tri-state with one reader now, and the two duplicate statics are deleted rather
than kept in step.
Verified with the profile summary on qwen3.6-35b-a3b 4-bit (`=0` logs
`keep_wired=false`, `=1` logs `true`), 1,407 package tests, a clean lint run, and
byte-identical goldens on qwen38-4 (the broad gate), qwen36-4 and agentworld-4.
ornith-8 is reported not checked — no install, and nothing is fetched to change that.
Engineering Notes carries the tri-state, why the duplicate readers are gone rather
than synchronised, and the unchanged-by-construction default path. The handover's
open item is removed in the checkout.
Tracker round 7: the Open table sorted easiest-first, and TT-030 closed
The table is now ordered by what each task costs to close, not by size alone, and the
rule sits next to it: a fast unit-suite change first (TT-008), then the intermittent
that needs an instrumented run under load (TT-001), then the measurements that need a
loaded model (TT-007, TT-011, TT-006, TT-005), then the engine work (TT-003, TT-002,
TT-004, TT-010, TT-009), then the two chores only the operator's account can close
(TT-018, TT-019).
TT-030 is closed with evidence: a session with no live agent reads its history from
storage through the harness's own cold reader (`sessionQuery.readSession` + a detached
`sessions.prepare`) and the same `deriveMessages()` the live path uses — verified live
against the private harness with no model server running, including the TT-014
session's assistant `42`. Engineering Notes records the path and why it is not a
hand-rolled parser.
Tracker round 6: TT-014 proven — a live prompt crossed the LAN Manager, and both write paths it drove were broken (TT-032)
TT-014 is closed with evidence instead of an assertion. The private harness on 7788
(`tools/dsh_local.sh`, pinned 0.1.6-alpha.2) with TinyTitanServer serving
`qwen3.5-4b_4-Bit` — an install already under `models/`, nothing fetched — was
driven with `curl`:
- `POST /dsh-lan/sessions` → `session-9bb889e8…`, preset `tinytitan`.
- `POST /dsh-lan/prompt` → `delivered: true`, message `87e73078…`.
- `GET /dsh-lan/sessions/:id/messages` → the assistant's `42`.
The answer is in the server's own log, so it is not a harness cache:
`chatcmpl-5d9c051f… prompt=7439 cached=0 completion=3 finish=stop` in 297.432 s, with
the 7439 prompt tokens being the harness's agentic system prompt (the harness also
issued its session-title call in the same second; the server's one-generation queue
serialized them). That first-call time is recorded as an observation, not explained.
The drive exposed TT-032, fixed in the same round. `POST /sessions` asked the
harness for `sessions`, which is `dsh-session`'s raw store rather than the
`sessionController` service, and died with `session header id "[object Object]" does
not match session id "[object Object]"`; `POST /prompt` built a user message with no
`source`, which the agent loop reads, so delivery said `delivered: true` and the turn
died 5 ms later with `Cannot read properties of undefined (reading 'kind')` — no
model call and no error at the route. Both are fixed in the checkout with regression
tests that pin the service name and the source, 105 plugin tests pass, and the
receipt's `wakeup` now reports what `followup` actually does.
Archive, workspace register, `startSession: true` and workspace delete with
`archiveSessions` were also driven live and are sound. Engineering Notes carries the
measurements and the "claim broader than the code" lesson.
Tracker round 5: TT-028 closed — a session is started by delegating to the harness
`POST /dsh-lan/sessions` and `workspaces`'s `startSession` now route through the
harness's own session controller rather than a reimplementation, with 501 kept for
profiles that compose none and the controller's own failure code preserved.
The page's "Not yet" section is rewritten to what is actually absent — a session
with no live agent has no readable history (TT-030) — and Engineering Notes records
that both previously-absent capabilities landed, with the reasoning for each.
Plugin tests are 104.
Tracker round 4: TT-029 closed — Bonjour is browse-only, by decision
The LAN source browses a service nothing registers. Registering it was rejected
rather than deferred: Bonjour advertises the host's LAN address, the API answers on
loopback only (TT-020), and a discovered peer that cannot be probed is worse than an
empty browse. The plugin now says so at every mount, and the page's discovery table
no longer implies Bonjour finds instances on the LAN.
Engineering Notes records the reasoning so it is not re-litigated, and the plugin
test count is current at 100.
Tracker round 3: TT-013 proven, and the defect it exposed fixed (TT-031)
TT-013 is closed with evidence rather than an assertion: two throwaway harnesses on
one machine, separate DSH_HOMEs on ports 3199 and 3198, each seeded with the other,
found each other in both directions at 9-15 ms. The first was booted before the
second, so its boot cycle could see nothing — which proves the periodic cycle and
not only the boot-time one.
The run also exposed TT-031, fixed in the same round: the shipped bundle patch
pinned `basePath` and `discoveryIntervalSeconds`, and a row config value beats its
environment fallback, so `DSH_LAN_DISCOVERY_SECONDS` and `DSH_LAN_BASE_PATH` were
documented but inert. Engineering Notes records both the proof and the defect, with
the note that a peer is a member only after a 200 from `/inventory` carrying the
shared key.
Tracker round 2: TT-017 closed — a session's history can be read back
The plugin could instruct a fleet and not collect the answers; it now exposes
`GET /dsh-lan/sessions/:id/messages`, built on the live agent's own session
derivation. The fleet-audit recipe in the LAN Manager page shows the read-back
instead of only the prompt, and the endpoint list and test count in Engineering
Notes are current (96 plugin tests).
Open is 17, Blocked 4, Parked 4.
Tracker round 1: three tasks closed, two found and added, sorted by size
The tracker is now ordered easiest-first with a Size column, so the work order is
the table order.
Closed:
- TT-015 — `apply()` now has a mount test: one prefix route at the base path, a
handler that answers, the mount banner, and disposal through both seams
(`ctx.effect` and `ctx.on('dispose')`). 92 plugin tests.
- TT-012 — the M1-M6 claim is now stated as what it is: `arm64` is required, every
published measurement is from one M3, and the wider range is intent, not a result.
- TT-016 — `POST /dsh-lan/gossip` is deleted. The mesh that works is the pull one:
`/inventory` returns each member's peer list and the table merges it. A mutation
endpoint with no caller is surface with no purpose, so it went, with its test and
its catalogue entry; `mergeGossip` stays, because the pull path uses it.
Found while reading the notes, and added:
- TT-028 — starting a session is not wired: the endpoint answers 501 and the trust
level is not accepted at all.
- TT-029 — Bonjour discovery can never find anything: the plugin browses
`_dsh-lan._tcp` and nothing registers it, so on a LAN with neither Tailscale nor
seeded peers it looks for records no machine publishes.
Re-sized TT-008 from S to M: the env is read in three places that each test
`== "1"`, so the fix is to single-source it as a tri-state — which touches the
runtime and therefore needs the golden check, not just a unit test.