Skip to content

the pin chip and the wire read one fact, a routing change lands on the next message, and a recording launch makes its own calls (#1022) - #1025

Merged
santoshkumarradha merged 11 commits into
devfrom
fix/1022-pin-chip
Sep 14, 2026
Merged

santoshkumarradha merged 11 commits into
devfrom
fix/1022-pin-chip

Conversation

@santoshkumarradha

Copy link
Copy Markdown
Member

Closes #1022.

Problem

Four things, all from the same drive.

  1. The chip said @morph while three turns were answered by Sail Research. The status chip, the settings panel's lane tail and the picker fold's mark were drawn from the profile row; the wire asked the transport. The two disagree the moment the wire retires a pairing.
  2. A routing change landed on the next session. Settings → Providers → routing wrote config.json, and the lane row beside it went on explaining auto in the launch word on the same screen.
  3. AFORGE_DEBUG=1 aforge wrote run.json and no request bodies. Four launches; aforge chat --debug wrote them all.
  4. The lane fold had no way back to auto (≈16 ↑ presses past every machine, and one too far lands on another model's row where enter switches the model), and its filter box filtered models, not machines.

Cause

  1. app.pinnedNow asked provider.PinnedFor, but laneNow, sheet.laneWord and armLanes's picker.pin all read config.LanePinned / config.LaneAt — the row on disk, which a retirement never touches.
  2. applyV3Governance read the routing row and wrote it onto session.Config.Routing, which newProviderClient handed every client as provider.StaticRouting(...). A handed-down answer beats the row InstallRouting puts in place, so the conversation's clients were pinned to the word on disk at launch. app.routing and sheet.routing were launch snapshots too.
  3. v3TakeHostRoad was given debug: *debug — the flag. With only the environment pin set, the launch took the host road, and the host process (never told to record) made the calls while this process wrote the header.
  4. applyLaneChoice only ever pinned; picker.rank cleared p.unfold before matching anything.

Fix

  • One door for the pin. provider.PinNow(model) answers the machine a request will demand and the machine a person's row still names that nothing is asking for, read together under one lock, with the model folded by lanePinFor — the same normaliser the retirement was written with, which also closes the surface-spelling seam noted on provider, config, tui3: simple routing — bare to OpenRouter unpinned, exactly the named provider pinned — is the row this build ships #1019. PinnedFor is its first half. tui3's laneInForce is the one reading every naming site makes: chip, laneNow, picker.force (beside picker.pin, which still tells the fold's three rungs apart), and laneRowTail. A retired pairing draws auto (morph cannot serve this model) on the model row and marks auto in the fold; the lane row itself is left exactly as written, because pinning again puts it straight back.
  • The routing row is installed, never handed down. The profile door no longer sets cfg.Routing; it installs the row (config.InstallRoutingRow, the half of InstallLaneRows the panel calls alone) and the session's clients read it live. Config.routingInForce() states the same rule for the gates on this side (the lane beat). app.routingRowChanged re-installs and re-reads app.routing / sheet.routing on the write.
  • The record's switch decides the host road, not one of its three doors: debug: trace.Enabled().
  • The fold's two gestures. enter on the machine in force unpins (keyed on what the wire would demand, so enter on a stood-down machine re-pins, as the manual promises); the hint slot reads enter unpin · ← back · esc. Typing while a fold is open matches that model's machines first, through the same tokenScore the model list uses, and falls through to the models when nothing matches.

Manual

models-and-cost.md (the "next session" claim and the model row's tail), lanes.md (a new section for the two gestures, and what the retirement does and does not touch), commands.md, keys.md, debug-record.md and staying-on-that-machine.md (a recording launch keeps its conversation in this terminal, by the pin as by the flag).

Evidence

  • internal/provider/pinchip_test.go — the law: over every state of the row (auto, openrouter, pinned, retired, re-pinned, base that carries no choice), what a surface may name equals what drawLaneChoice demands for a call a person is reading.
  • internal/tui3/pinchip_test.go — the surface reads the transport and not the row; laneRowTail over its states; the unpin toggle and its hint; the fold's filter; a routing cycle reaching the transport, app.routing, the lane row's explanation and the fold's sentence on one frame.
  • cmd/aforge/recordroad_test.go — the host road is decided by the switch (AST law) and refuses a host while the record is on.
  • cmd/aforge/routinglive_test.go, internal/session/routinglive_test.go — the door hands down no row and installs one; a session with no row of its own follows the live one, a session carrying one keeps it.
  • Spark gate (make test-laws, make pr-ready BASE=origin/dev) and a real-model drive: pasted into a comment below.

Not verified

🤖 Generated with Claude Code

https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv

santoshkumarradha and others added 6 commits September 13, 2026 22:15
…ile the wire would demand it, a routing change lands on the next message, and a recording launch makes its own calls

Issue #1022 saw `moonshotai/kimi-k3@morph` on the status chip over three turns
another machine answered. The chip, the panel's `lane` tail and the fold's mark
were drawn from the profile row while the wire asked the transport, and the two
disagree the moment the wire retires a pairing. They now read one door —
`provider.PinNow`, which answers the machine a request will demand and, beside
it, the machine a person's row still names that nothing is asking for — and the
model is folded by the transport, under the same normaliser the retirement was
written with, rather than against a key built on the surface's own spelling.

A routing change landed on the next session rather than the next message,
because the chat door read the row and wrote it onto the session config, which
every client then held as an answer of its own — and an answer handed down beats
the row this process installs. The door now installs the row and hands down
nothing, so the conversation's clients read the live one; the settings panel
re-installs it and re-reads its own two fields in the same keystroke. An engine
host and a task child still carry their parent's row explicitly and still win.

`AFORGE_DEBUG=1 aforge` wrote a folder holding run.json and no request bodies,
because the host rule read the --debug FLAG while the record has three doors.
It reads the switch, so the environment pin keeps the calls in this process
exactly as the flag does.

And the fold got the two gestures it was missing: `enter` on the machine already
in force takes the pin off, and typing while a fold is open filters that model's
machines before it filters the models.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv
The row is installed on the process-wide knob rather than handed down onto the
session's config, so the accessor the v3 door used to touch is no longer called
from outside this package. The law that every persisted row names a reader
something outside internal/config really calls now names InstallRoutingRow,
which is what the launch and the settings panel both call.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv
…ting row

No door in this binary hands a routing row down any more: the engine host builds
its session through the same profile door, and the two in-process auxiliaries
carry a parent field that is now empty. The seam is still there and a caller that
holds a row this process did not install still wins with it — which is what the
comments now say, instead of naming call sites that do not exist.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv
The tail on the model row was asking the transport directly, which skips the
four states where this surface may name no machine at all — a hosted window,
routing off, no model, a model served direct. It now goes through the same door
the chip does, as a door rather than a snapshot, because a pin written two rows
above it moves the answer in the same keystroke. The one reading that had no
caller is gone.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv
@santoshkumarradha

Copy link
Copy Markdown
Member Author

Real-model drive — Spark, moonshotai/kimi-k3, one process, aforge chat --debug

spark:~/af-1022-drive.sh, evidence under spark:~/af-1022-evidence/drive/ (screens + the whole logs/ tree). Home /tmp/a22/t, tmux 200×50, a few cents.

A routing change lands on the very next request, with nothing relaunched

Launched on the shipped row. Turn 1's body:

1c3695c3.json {"model": "moonshotai/kimi-k3", "provider": null}

ctrl+, → typed routing → enter. The row and its own sentence, on the same frame:

 routing                                              • latency
    one model is served by many endpoints. simple is the one it ships with … 
    a change here takes effect on your next message.

Turn 2, same process, no relaunch:

9abae6b9.json {"model": "moonshotai/kimi-k3", "provider": {"sort": "latency", "order": ["Sail Research"],
               "allow_fallbacks": true, "require_parameters": true, "max_price": {...}}}

And the surface followed it on the same frame: before the cycle the lane row's explanation read "routing is simple, so auto sends no choice of ours at all"; after it, /model's fold drew the other sentence —

    ● auto    router routes; aforge takes over if answers turn bad — wafer now · recommended

The pin goes on, and comes off with the same key

/model → → → ↓ → enter pins parasail. The chip, and turn 3's body and who served it:

─ … · moonshotai/kimi-k3@parasail · ⠿ auto · via parasail · main ─
070413b0.json {"model": "moonshotai/kimi-k3", "provider": {"only": ["Parasail"], "allow_fallbacks": false, …}}
{"id": "070413b0", "tag": "turn", "lane": "Parasail", "served": "Parasail", "status": 200}

/model → → walks straight in onto the machine in force, and the hint slot says what enter does there:

─ … · moonshotai/kimi-k3@parasail · ⠿ auto · via parasail · main ──── enter unpin · ← back · esc · crew balanced ─
›     parasail                                  1.0s · 29 t/s · $15/M · bad replies · 98% · ▇

One enter, and the chip loses the machine — moonshotai/kimi-k3 — while turn 4 goes out with no demand on it and is answered elsewhere:

e51108f3.json {"model": "moonshotai/kimi-k3", "provider": {"sort": "latency", "order": ["Parasail", "Sail Research"], …}}
{"id": "e51108f3", "tag": "turn", "served": "Sail Research", "status": 200}

The fold's filter filters the fold

With the fold open, typing a keeps it open and keeps the machines whose names carry the letter — fireworks and chutes are gone, everything else stays:

 › a
  moonshotai/kimi-k3       via wafer · ▲0.7s · $2.6/$13 per M · 1M · 45t/s · elo 1425 · sees · watches
    ● auto              router routes; aforge takes over if answers turn bad — wafer now · recommended
›     parasail / sail research / makora / wafer / baseten / relace / alibaba / deepinfra / phala

🤖 Generated with Claude Code

https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv

@santoshkumarradha

Copy link
Copy Markdown
Member Author

AFORGE_DEBUG=1 aforge — the bare door, no flag

spark:~/af-1022-env.sh, evidence at spark:~/af-1022-evidence/env/. One turn, then what the run folder holds:

/logs/trace/40f9a5786f7d6b22/run.json
/logs/trace/40f9a5786f7d6b22/calls/e2b2e84b.json
/logs/trace/40f9a5786f7d6b22/calls/0770937b.json
/logs/trace/40f9a5786f7d6b22/calls/5f879a4a.json
### bodies: 3

Before this change that folder held run.json and nothing else, four launches running — the header written here, the calls made by the session host.

Reproducing the anomaly — three attempts, not reproduced

spark:~/af-1022-repro.sh, run three times (spark:~/af-1022-evidence/repro{1,2,3}/). Each walks the issue's own sequence on moonshotai/kimi-k3 with routing: simple: ctrl+, → lane → enter → walk to a machine → enter, two turns, quit, relaunch, one turn — with --debug, so every body is on disk.

The row after the pin, and the chip on the relaunched process:

{"lane.talk": "Parasail", …}
─ … · moonshotai/kimi-k3@parasail · ⠿ auto · main ─

Every turn call in all three runs, before and after the relaunch, carried the demand and was answered by the machine it named. No turn went out bare while the chip named a machine, and no turn was served elsewhere:

{"tag": "turn", "lane": "Parasail", "served": "Parasail", "status": 200}   ×9 across the three runs

But one line in run 3 has the reported shape, and it is not a defect

{"tag": "title"}
{"tag": "title", "served": "Sail Research", "status": 200}

A title errand on the conversation's own model, going out with no provider object at all and answered by Sail Research — while the chip correctly read @parasail and every turn beside it demanded Parasail. That is the simple row's documented scope since #1019: the pin rides the calls a person is reading (lane.talk), and the title, the memory reflex and the route question go bare.

The issue's evidence is calls.jsonl: "served":"Sail Research" on moonshotai/kimi-k3 lines while the chip read @morph. A title line looks exactly like that. The turn lines carry a lane field and the errand lines do not, which is how the two are told apart. This is a hypothesis about that drive, not a finding — the original run's bodies are gone (AFORGE_DEBUG=1 wrote none, which is the papercut fixed above), so it cannot be checked.

Not reproduced in three attempts. Item 1's law ships anyway: after it, no surface can name a machine the wire is not asking for, which is the screen the report describes whatever produced it.

🤖 Generated with Claude Code

https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv

santoshkumarradha and others added 2 commits September 13, 2026 22:40
Found by the Spark gate: `core` matches Cloudflare too, because c-l-o-u-d-f-l-a-r-e
carries those four letters in order and the third matching rung is a subsequence.
Keeping them in the ledger's order put the cursor on the machine nobody typed for,
so the matched machines are sorted by the same summed rungs a model id is scored
by — a prefix first, then a substring by where it starts, then the loose letters —
with ties keeping the order the fold draws when nothing is typed.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv
…machines

The model list's own loop and the fold's were the same summed rungs written
twice; the list now asks queryScore as the fold does, and the fold's two
parallel readings ride a named pair instead of an anonymous one.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv
@santoshkumarradha

Copy link
Copy Markdown
Member Author

Spark gate — green at 1087d9717

~/af-1022b (a detached worktree at this branch's head), make test-laws then make pr-ready BASE=origin/dev:

LAWS_EXIT=0
touched: ./cmd/aforge ./internal/config ./internal/provider ./internal/session ./internal/tui3
ok  	github.com/Agent-Field/aforge-v2/cmd/aforge	68.121s
ok  	github.com/Agent-Field/aforge-v2/internal/config	2.782s
ok  	github.com/Agent-Field/aforge-v2/internal/provider	71.105s
ok  	github.com/Agent-Field/aforge-v2/internal/session	184.524s
ok  	github.com/Agent-Field/aforge-v2/internal/tui3	311.043s
PRREADY_EXIT=0

Laptop: go build ./... && go vet ./... && gofmt -l internal/ cmd/ clean.

One defect the gate found, and it was mine. TestTypingInsideAnOpenFoldFiltersTheMachines failed on the first run: typing core in an open fold matched Cloudflare as well, because c-l-o-u-d-f-l-a-r-e carries those letters in order and the filter's third rung is a subsequence. Keeping the ledger's order there put the cursor on the machine nobody typed for. The fold's matched machines are now sorted by the same summed rungs a model id is scored by (32f7b681d), and the model list and the fold ask one scorer rather than two copies of the same loop (1087d9717).

🤖 Generated with Claude Code

https://claude.ai/code/session_0182eyxXP8Xe7ADNKpYDe4wv

@santoshkumarradha
santoshkumarradha marked this pull request as ready for review September 14, 2026 02:53
@santoshkumarradha
santoshkumarradha merged commit 0738312 into dev Sep 14, 2026
2 checks passed
@santoshkumarradha
santoshkumarradha deleted the fix/1022-pin-chip branch September 14, 2026 02:54
@santoshkumarradha santoshkumarradha added the bug Something the code does that it should not label Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something the code does that it should not

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lane pin from the settings sheet: three turns served by another machine while the chip read @morph (seen once, dev 649392ad7)

1 participant