Skip to content

[Feat]: forward all samplers and parse full /props for llama.cpp - #895

Draft
a-ghorbani wants to merge 24 commits into
mainfrom
feature/TASK-20260903-1831
Draft

[Feat]: forward all samplers and parse full /props for llama.cpp#895
a-ghorbani wants to merge 24 commits into
mainfrom
feature/TASK-20260903-1831

Conversation

@a-ghorbani

@a-ghorbani a-ghorbani commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Remote llama.cpp servers currently receive only temperature, top_p,
max_completion_tokens, stop, stream, tools, tool_choice,
response_format and the reasoning kwargs. Every other sampler the UI exposes is
dropped on the floor, so those controls lie. /props is read for n_ctx and
modalities.vision only. Remote turns show no timings.

This changes three things.

1. Forward the full sampler set

top_k, min_p, typical_p, xtc_probability, xtc_threshold, mirostat,
mirostat_tau, mirostat_eta, seed, n_probs and the four penalties now
reach the server.

Read and write are deliberately separate:

  • PARAM_WIRE_NAME — one table, all sixteen numeric controls, used to read
    server defaults out of /props.
  • FORWARD_ALLOWLIST — per server type, decides what is written into a
    request body.

One name table, two policies. A server type that has not been measured gets a
smaller allowlist without needing a second copy of the names.

Four of the sixteen need renaming on the wire. PocketPal's internal
penalty_last_n / penalty_repeat / penalty_freq / penalty_present are
not llama-server field names — they are silently ignored. The correct names
are repeat_last_n / repeat_penalty / frequency_penalty / presence_penalty.

2. Parse the whole /props response

The response is split into three tiers by lifetime, not by subject:

tier persisted what it is
caps yes things that gate behaviour — context length, vision, audio
props yes descriptive — server defaults, build info, model alias
presence no volatile — is_sleeping, slot counts

Persisting a volatile field is how a capability model goes stale without
anything reporting it, so presence is deliberately not persisted.

The sampler settings sheets now show the server's own default next to each
control, with a per-parameter reset. The router placeholder response
(role: "router", model_path: "none", params: null, n_ctx: 0) is
recognised and rejected rather than parsed as if it described a model.

3. Per-turn timings for remote turns

The footer renders predicted_per_second, prompt_per_second and cache_n from
the streaming finish chunk. A reported zero is kept distinct from an absent
value — 0 cached and "no number" are different facts and must not collapse.


Wire verification

Every claim below was measured against a running llama-server, not read from
documentation. Six raw response bodies were captured verbatim, and every
wire-shape fixture in these tests is copied from one of them character for
character
— none is hand-authored or transcribed from a README. A wire name
hand-authored in both the code and the fixture lets a typo pass in both: the test
then proves the parser agrees with itself rather than with the server. This
server was measured contradicting its own documentation more than once, so a
doc-derived fixture would have encoded that error and gone green.

Environment: llama-server in router mode, build b9976-e3546c794, model
bartowski/Qwen_Qwen3-1.7B-GGUF:Q4_K_M, 4 slots, n_ctx 8192.

Samplers that landed

Sent with distinctive values, then read back from GET /slots?model=<id> on the
slot with the highest id_task. A value differing from the server default proves
the request name was honoured.

top_k: 1111 · min_p: 0.110.10999999940395355 ·
typical_p: 0.910.9100000262260437 · xtc_probability: 0.21
0.20999999344348907 · xtc_threshold: 0.310.3100000023841858 ·
mirostat: 22 · mirostat_tau: 4.14.099999904632568 ·
mirostat_eta: 0.210.20999999344348907 · seed: 1234512345 ·
n_probs: 22 · temperature: 0.330.33000001311302185 ·
top_p: 0.770.7699999809265137

Samplers that did NOT land — the negative result

Sent as penalty_last_n: 41, penalty_repeat: 1.11, penalty_freq: 0.41,
penalty_present: 0.51, the slot kept its defaults (64, 1.0, 0.0, 0.0)
and the request still returned 200. Re-sent under llama-server's own names,
seconds apart, same instance, same model, same slot pool, same body shape — only
the four field names differed — all four landed: 41, 1.1100000143051147,
0.4099999964237213, 0.5099999904632568.

The second request is what makes the first one mean something. Without it, the
unchanged defaults would only show that something went wrong — a typo, a wrong
endpoint, a server that ignores penalties entirely. The control rules those out.

Reasoning: one scope line was removed because it does nothing

request HTTP reasoning_content verdict
no reasoning params 200 "Okay, the" model thinks by default
chat_template_kwargs:{enable_thinking:false} 200 null works
reasoning_effort:"none" 200 "Okay, the" silently ignored
reasoning_effort:"none" + reasoning_format:"auto" 200 "Okay, the" silently ignored
reasoning_budget_tokens: 512 200 "Okay, the" inert on this build
totally_bogus_key_xyz: 123 200 "Okay, the" unknown keys accepted silently

reasoning_effort is not a llama-server request field — it is absent from
tools/server/server-schema.cpp upstream and is ignored here. Shipping it as an
off switch would have been green and done nothing. enable_thinking remains the
sole carrier of on/off. reasoning_budget_tokens is the correct upstream
name (-1 .. INT32_MAX, -1 disables); it is inert on this build and honoured
on newer ones, and an ignored numeric field cannot break a request.

The last row of that table also sets the failure mode for this server type: a
silent no-op, never a rejection.

Timings

The finish chunk of a stream: true completion carries timings with
predicted_per_second, prompt_per_second and cache_n — all three present.
No usage object appears anywhere in the stream without an explicit
include_usage opt-in, which is why cached tokens are sourced from
timings.cache_n and not usage.cached_tokens.

Why the read-back is a required step and not a note

A wrong wire name produces no error, no log line, and nothing a user could tell
apart from ordinary model variance. The consequence is silent and no symptom
will ever surface it, so this observation is the only evidence that will ever
exist.
No amount of additional upstream testing substitutes for it: a test
appearing tomorrow would raise confidence that the names are right, and leave
the check that they landed exactly as unsubstitutable.

Two nearby signals are not weaker versions of it — they are coincidences that
read as confirmation:

  • "The completion returned 200." Measured above: unknown body keys return
    200. A status code says nothing about any field.
  • "The output changed when I moved a sampler." Ordinary model variance
    produces that with no sampler applied at all, and a seeded comparison would
    require seed itself to have landed — one of the sixteen names in question.

Three claims not to make about this evidence

Each was written, believed, and corrected while building this. Kept in the wrong
form on purpose: a reader arriving with the wrong version already in their head
recognises it and stops, where they might read past the right version without
noticing they disagree.

  1. Not: "upstream asserts the timings fields the footer renders." The
    upstream assertions are on POST /completion, read non-streaming from
    res.body. The footer reads the OpenAI-compatible /v1/chat/completions SSE
    finish chunk — a different endpoint and a different delivery mode.
  2. Not: "upstream asserts total_slots for the call this code makes."
    Upstream hits bare GET /props on a direct single-model server. This code
    issues /props?model=<id> against a router, where the bare form returns the
    placeholder.
  3. Not: "a trailing slash in a server URL breaks every request." It does
    not — normalizeUrl strips trailing slashes at all four request-construction
    sites. This was measured against the server directly, which bypassed the app's
    own normalisation. A /v1 suffix is fatal and is a separate fact.

The shape common to all three: the measurement was correct and the scope of the
claim was not. Check the endpoint, the accessor, the delivery mode, and whether
the app's own code sits between you and the wire.

Limits

Every fact above was measured on one build, b9976-e3546c794. Generalisation
across builds is unverified.
Users point the app at whatever build they run.

Most of it fails safe: an absent /props key leaves the field unknown by
construction, absent timings fields drop that part of the footer, and an ignored
numeric field is inert. The sixteen sampler names are the exception — a
rename would stop them landing, silently. That degrades to today's behaviour
rather than regressing past it, but it is not safe, which is why the read-back is
a step rather than a footnote.

Still unverified, labelled as such rather than assumed:

  • reasoning_budget_tokens and chat_template_caps.supports_thinking on builds
    newer than b9976. An absent capability key reads unknown, never false.
  • vLLM's acceptance of top_k / min_p / repetition_penalty.

What the captures cover, and what they do not

Four captures are posted on this PR.
They evidence the remote surfaces only:
the two sampler sheets showing the server-default indicator and its reset, and
the remote turn footer cold and warm — the warm one showing prompt speed
150 → 1406 tok/s with 0 cached12 cached, which demonstrates the
reported-zero-vs-absent rule inside a single capture.

They do not cover the local turn footer. The footer parts here are gated on
origin, so a local turn must render neither of them, and there is no capture of
that. It is carried by a paired test instead, deliberately: the claim is an
absence, and a screenshot of an absence cannot distinguish "correctly
suppressed" from "the build was stale" or "that turn never populated timings".
The test asserts both arms from one timings object under one harness —
suppressed on a local turn, present on a remote one — because a test asserting
only the local absence would pass equally well if the whole footer were broken.

Stated because an evidence set with an unstated boundary gets read at its widest,
and a capture cannot correct for that itself.

These captures were blocked from upload for several hours, by two independent
faults, either of which was sufficient on its own
. Recorded here rather than
dropped, because a public artefact that names one cause for a two-cause failure
is its own defect:

  1. Environment. Both browsers on the build host are snap installs, so their
    profiles live under ~/snap/..., while the upload tool searches only the
    classic paths. It could not read a cookie store at all. Fixed by bridging the
    snap profile into ~/.mozilla/firefox — and a directory symlink was not
    enough, because the tool walks directories without descending into symlinked
    ones; it needed file-level links.
  2. Tooling. The posting helper passed --repo to a subcommand that rejects
    it, read the resulting non-zero exit as "no valid credential", and discarded
    the stderr that said otherwise.

Both were live the whole time, and the environment one came first — so fixing
either alone would have changed nothing. An earlier revision of this section
named only the second, which understated it.

This was not a false alarm. The gate was right that the upload could not
proceed — it was wrong about why, and it destroyed the message that would have
said so. Two live faults produced one identical symptom, and the only text that
distinguished them was generated and discarded on every run. Re-running the
check would not have helped; reading its stderr would have.

Capture provenance

The captures were taken at 07:40-07:41 from an APK built at 07:38, which is the
right order — but two later commits (10:06) touch exactly the two captured
surfaces, so the captures predate them and were deliberately not retaken.

That carry-forward is not assumed, it is shown: current code was rendered under
each capture's own conditions and reproduces them — the sampler sheet still
offers Server default: 0.8 · Reset on temperature with the other three reading
as on-default, and the warm footer still produces
7ms/token, 135.81 tokens/sec, 1406.35 prompt tokens/sec, 12 cached, 107ms TTFT.
The check discriminates: substituting an out-of-range server default makes the
sampler assertion fail.

Worth stating because a stale capture is harder to catch than a stale build. A
stale build fails a content check — the new string is simply absent. A stale
capture is a real screenshot of a real build, with the right name, surface and
resolution; nothing about the file is wrong except which build it depicts, and
no inspection of the image can reveal that.

Reading the diff

ChatGenerationSettingsSheet.tsx and PalGenerationSettingsSheet.tsx show ~830
changed lines but are 33 insertions / 12 deletions under git diff -w. The
rest is a forced reindent from wrapping both in observer(...). Read those two
with -w.

Tests

274 suites, 4446 passed, 2 skipped. Lint and typecheck clean against baseline;
l10n:validate passes. No native changes.

A misspelled sampler name used to index a Partial<Record<SamplerParam, number>>
as a plain string, which yields any under this project's noImplicitAny
setting — the server-default indicator would have silently never rendered for
that control. Naming the parameter makes it a compile error, and doing so
surfaced two further errors the loose index had been hiding.

Docs

The architecture record for remote servers is updated in the same round as this
code, in context/architecture/remote-servers.md.

Generated by PocketPal Dev Team

resolveRemoteProps had no direct test: only samplerDefaults was reached,
so a dropped slotCount, buildInfo, modelAlias or chatTemplateCaps
passthrough would have been silent.

The pal sheet's server-default test asserted a first render, which passes
without observer; a probe that lands while the sheet is open does not.
A misspelled control name indexed a Partial<Record<SamplerParam, number>>
as a plain string, which yields `any` under the project's noImplicitAny
setting: the server-default indicator would silently never render for that
control, with no error anywhere. Naming the parameter surfaces the typo, and
surfaced two latent errors the loose index had been hiding.

capsMatchBinding took a structural `{probedUrl?: string}`, which any of the
three tiers satisfies, defeating the discriminant that is supposed to keep a
descriptive record out of a capability position.
The mock re-implemented the lookup verbatim, so the two could drift without a
test noticing. Both now call the same function.

The name says what the value is: the last observation, never expired here, so
a consumer needing freshness bounds it against `at` itself.
@a-ghorbani

Copy link
Copy Markdown
Owner Author

Posted as a comment rather than a formal Request changes review: the bot identity was unavailable (GH_APP_ID unset), so this ran under the PR author's own account and GitHub refuses a self-review. The verdict below is REQUEST_CHANGES and should be read as one.

Independent review

verdict: REQUEST_CHANGES
role_subreviews: COMPLETED (8/8 + an independent refutation pass)
review_complete: yes
scale: 29 files, +2932 / −540

No blocker in the code. One blocking process condition, seven concerns, and a
long tail of suggestions. The change is well built and unusually well evidenced;
most of what follows is about reach beyond the stated scope, not about the wire
work itself.


Blocking

BL1 — No visual evidence on the PR. gh pr view 895 → no images in the body,
zero comments. AGENTS.md makes this blocking on its own for a change with
visible UI.

The cause is environmental, not authorial: tools/post-pr-visual-evidence.sh
exits 3 with MANUAL_POST_REQUIRED — no GitHub session token for the image
upload. Four captures exist and were verified by reading them.

It is worse than a missing upload, because of C2 below: the four captures cover
the remote surfaces only, and this change also alters the local turn
footer, for which there is no capture at all and no mention in the PR body.


Concerns

C1 — "Reset to server default" writes a value the app's own validator then
rejects.
src/components/CompletionSettings/CompletionSettings.tsx:83-95.

Found independently by five reviewers. The reported default is passed to
onChange unclamped, while the same component holds the metadata that bounds
the control. --top-k 0 (the standard way to disable top-k) and
--repeat-last-n -1 are ordinary llama.cpp configurations; the app's ranges are
top_k min 1 and penalty_last_n min 0. Proven by probe:

onChange: ('top_k', 0), ('penalty_last_n', -1)
validateCompletionSettings → isValid:false
  top_k: "Value must be between 1 and 128"
  penalty_last_n: "Value must be between 0 and 256"

The chip renders and presses; Save then aborts with an alert naming a parameter
the user never typed, with no highlight on the offending control and no clean
recovery but discarding the draft. Once written, the tolerance check matches, so
the affordance collapses to the plain "server default" label while the clamped
slider shows a different number.

Fix: return null when the reported value fails the control's own validation.

C2 — The "remote turn footer" change fires on every local turn, undescribed,
untested and uncaptured.
src/components/AssistantTurnFooter/AssistantTurnFooter.tsx:66-81.

Raised by two reviewers; I verified it directly and both understated it. The
footer gates on presence (!= null), not origin. llama.rn declares
NativeCompletionResultTimings with both cache_n and prompt_per_second
as required, non-optional numbers, and useChatSession.ts spreads engine
timings with no origin branch. Neither field renders on origin/main.

So every local turn footer gains two new metrics, not one. Both new
footer tests build from the remote finish-chunk fixture, and every local-turn
fixture omits the fields, so nothing asserts the local string.

Fix: decide deliberately — either gate on origin, or accept the local change,
describe it in the PR body, test it, and capture it.

C3 — Server sampler defaults are rendered on, and reset into, two scopes that
are not the session's model.
PalGenerationSettingsSheet.tsx:252,
ChatGenerationSettingsSheet.tsx:362.

ModelStore.activeSamplerDefaults resolves against activeModel and the live
binding. Its binding guard is careful on the axis it checks, but both call sites
hand the result to a surface editing a different scope: the Pal sheet edits a Pal
carrying its own defaultModel, and the chat sheet edits the app-wide preset
whenever isEditingPresetSettings. One server's numbers are therefore labelled
"server default" for, and reset into, settings belonging to a different model —
and the row appears or vanishes with whatever happens to be loaded.

context/architecture/remote-servers.md:1163 asserts the opposite for the Pal
case; the preset case has no row at all.

C4 — The volatile tier is structurally assignable into both persisted tiers.
src/utils/types.ts:536-540.

RemoteModelPresence carries no tier field, and every field of both persisted
types is optional. Verified under tsc --noEmit --strict: props→caps, caps→props
and the corresponding Record conversions all error; presence→caps,
presence→props and Record<string,Presence>Record<string,Caps> produce no
diagnostic
. this.remoteCaps[key] = presence compiles and would persist a
volatile claim.

remote-servers.md:419-423 (restated at :886) publishes the opposite as a
structural invariant. Fix: add tier?: 'presence', and correct the doc.

C5 — Unbounded wire strings are persisted, and on Android one probe can break
persistence of the whole store.
src/api/openai.ts:414, :550-558;
ServerStore.ts:127-137.

nonEmptyString applies no length cap to build_info or model_alias. Every
sibling field is properly bounded. Survived an adversarial attack and came back
stronger: driving the real fetchServerProps with a 4 MB build_info produced a
persisted blob of 8,388,648 bytes — one probe of one model, no accumulation.
The Android ceiling is real and traced through the vendored source
(config.gradle dbSizeInMB = 6LReactDatabaseSupplier.setMaximumSize, no
override in android/gradle.properties). Because makePersistable writes one
record for the whole store, servers and privacyNoticeAcknowledged go down
with it.

Two qualifications from the refutation, both material: the consequence is
Android-only (iOS spills values over 1 KB to uncapped files), and unbounded
wire strings already reach the blob on main via RemoteModelInfo.id. But
remoteProps is new here, and nothing reads either field today — so capping or
dropping them is nearly free.

C6 — The new probe coalescing is a PR-introduced regression on two recovery
paths.
ServerStore.ts:82-84, 373-391.

probesInFlight is absent from main. Proven by test: a second caller arriving
while a probe hangs issues zero new HTTP requests and writes nothing. Two
consequences:

  • The iOS foreground reprobe (ModelStore.ts:1214-1215) exists precisely because
    the first probe is the request that raises the local-network prompt, so a grant
    always arrives after it has already failed. The reprobe now joins the doomed
    request instead of issuing one under the new grant. The window is ~10 s, not 5
    probeRemoteModel makes two sequential fetchServerProps legs and the
    .finally hangs off the whole thing.
  • The dedup key carries no url, so a refresh issued after a server url edit joins
    the pre-edit promise: fetchServerProps calls: 1, urls asked: ["http://old:8080"], both maps empty, and the caller resolves as success.

Impact is narrower than first reported: the reprobe fires on every foreground
transition and a settled probe releases the entry, so it self-heals on the next
cycle rather than staying unknown for the session. Fix: clear the map on leaving
active, or add a force flag; and put the url in the key.

C7 — PROPS_SCALAR_FIELDS reintroduces exactly the drift CAPS_FIELDS exists
to prevent.
ServerStore.ts:41-49.

Two hand-maintained lists sit directly under a comment reading "Enumerated once
so the usability check and the no-op write check cannot drift apart."
Adding a
scalar to PROPS_FIELDS alone makes samePropsContent report true for a
changed answer, and the write is silently skipped — in a map this PR has just
made persistent.


Suggestions

# Finding Where
S1 Footer .toFixed() on never-validated wire numbers throws in render with no error boundary, and the value persists so it recurs on reopen. Downgraded: a hostile server already has the identical crash+persist primitive on main via predicted_per_second; this adds a third key to a saturated primitive. Still one line to fix with the existing finiteNumber helper. AssistantTurnFooter.tsx:66-81, openai.ts:1009
S2 The reset writes the raw double while the label rounds, so 0.800000011920929 resurfaces in the numeric box on the next sheet open. The PR's own test pins the current behaviour. CompletionSettings.tsx:85
S3 The reset affordance: no accessibilityRole="button" (159 precedents in-repo), a ~16-20 dp target that silently spans the full card width with no hitSlop, theme.colors.primary is body-grey in this app so nothing looks tappable, and disabled renders identically. CompletionSettings.tsx:84-93, styles.ts:27-30,73-77
S4 The per-serverType payload table — the one doc that governs "is this field safe for this server" — was not updated for reasoning_budget_tokens. openai.ts:669-687
S5 The whole presence tier and four of five remoteProps fields have no non-test consumer, and four of them are now persisted. Persisted schema is the expensive kind to carry. types.ts:516-540
S6 removeUserSelectedModel prunes none of the four per-model maps; a removed-then-re-added model resurrects its old entry. ServerStore.ts:208-212
S7 FORWARD_ALLOWLIST admits n_predict, whose send name is max_completion_tokens; inert only because StreamChatParams has no such field. type ForwardableParam = Exclude<SamplerParam,'n_predict'>. openai.ts:112
S8 utils/types.ts importing SamplerParam from api/openai. Downgraded: type-only, all fourteen imports erase, nine pre-existing precedents, and the proposed move splits llama-server HTTP vocabulary into a llama.rn module and separates it from FORWARD_ALLOWLIST. types.ts:14
S9 activeSamplerDefaults depends on the whole capabilityEnv while reading two of seven fields, dragging an O(servers × models) rebuild along. Zero incremental cost today because the chain is already observed. ModelStore.ts:2607-2610
S10 Presence is the one tier written with no no-op guard, and it is minted with a fresh at on every probe. Free today; the asymmetry is invisible to the next author. ServerStore.ts:481-483
S11 Fixture provenance is unenforceable from this repo — the captures live in the dev-team repo, so no CI check can catch a future hand-edit. The header's "not reformatted" is also literally untrue (JSON→TS transcription), though the substance verified. A local HF cache path is committed in model_path. jest/fixtures/llamaServerWire.ts
S12 Two comments justify declarations on "nothing does this yet" grounds — case 3 under the four-case test, and symptomatic of S5. listCaps.ts:16, modelCaps.ts:26
S13 renderServerDefault(name, 0) at CompletionSettings.tsx:152 is unreachable. FORWARD_ALLOWLIST and REASONING_BUDGET_TOKENS are keyed by string where ServerTypeOption and EffortLevel exist — an added effort level fails silently to -1, uncapped. various
S14 Localization polish: "{{value}} cached" drops its noun; casing flips between the two states of the same row. en.json:567,688-689

Verified clean — recorded so a follow-up pass does not re-derive it

  • Fixture provenance. Two reviewers independently deep-compared all five
    exports against the capture files: identical, key order included, down to the
    6 KB Jinja template and unused lora/speculative noise. Projection is real
    (penalty_last_n: wire.repeat_last_n,
    Object.keys(slotsAfterSamplerRequest[0].params)), not decorative.
  • PARAM_WIRE_NAME 16/16, checked against slot id: 3 of the read-back
    capture — every name confirmed accepted, not merely present. The four
    penalty_* spellings appear in neither capture, which positively confirms the
    renames are required.
  • FORWARD_ALLOWLIST keys match SERVER_TYPE_OPTIONS and
    detectServerType exactly.
  • Falsifiability. Six mutations across two reviewers, every one caught by a
    named test, all files restored.
  • Hygiene. Zero internal tracker IDs or story anchors across every added
    line and all 17 commit messages.
  • Comments. 160 added lines against 2513 added non-whitespace (6.4%),
    overwhelmingly case 4 — proportionate for a wire-contract boundary. Three
    exceptions flagged by case number.
  • Hydration and downgrade. An absent supportsAudio reads 'unknown'
    end-to-end, never a definite false; pre-probedUrl entries are dropped
    whole; rollback is safe with no migration.
  • Key shape has no collision and no cross-server prefix match.
  • remotePresence is genuinely absent from the makePersistable list,
    verified in the call rather than the comment.
  • Nothing added to the streaming hot path; no bundle-size change; no new
    endpoint, permission or manifest surface; the only wire value reaching a URL is
    encodeURIComponent-encoded.
  • NATIVE_CHANGES=NO, re-derived from the diff by two reviewers with the
    commit-scoped check. pod install and the two platform builds are not
    required.

Verification

tsc clean · eslint 0 errors on PR files · jest 275 suites / 4451 passed /
2 skipped · l10n:validate valid · native not required. Full detail, including
a lint false positive caused by a concurrent reviewer's scratch file, is in
verification.md.

Residual risk

  • Behaviour on a llama-server build newer than b9976-e3546c794 is unverified.
    The author states this limit rather than generalising from it, and the failure
    mode for every field except the sampler names is fail-safe.
  • vLLM's acceptance of the sampler set is unverified; its allow-list row is
    empty, so nothing is sent.
  • No device run beyond the four captures, none of which covers the local footer
    change in C2, or Android/iOS divergence on the new row.

Open questions for the author

  1. Is the local-turn footer change (C2) intended? The PR presents the footer work
    as remote-only.
  2. Should the presence tier and the four unread remoteProps fields ship now
    (S5), given they are persisted and have no consumer in this change?
  3. C3 asks a product question, not just a code one: what should a Pal or preset
    sheet show when the "server default" belongs to a model that is not the one
    being configured?

Eight role reviewers plus an adversarial refutation pass, none of them the implementer, against a separate worktree built from the PR ref. Two single-sourced concerns were downgraded by the refutation and one had its impact corrected; the surviving ones are recorded with what was done to try to break them.

Generated by PocketPal Dev Team

llama.rn reports prompt_per_second and cache_n on local completions too, and
both are non-optional there, so gating on presence silently added two metrics
to every local turn footer. Origin gates them instead.

The test asserts both arms from one timings object: suppressed on a local
turn, present on a remote one.
A server run with `--top-k 0` reports a default outside this app's range, so
the reset offered a one-tap write that saving then rejected with an alert
naming a parameter the user never typed.
@a-ghorbani

Copy link
Copy Markdown
Owner Author

Two review findings fixed

The footer parts are now gated on origin, not presence (bc8ec498).

llama.rn declares both cache_n and prompt_per_second as required,
non-optional numbers on local completions, and neither renders on main — so
gating on presence silently added two metrics to every local turn footer.
That was an accident, not a decision: undescribed, unasserted and uncaptured.
The local footer is worth having, but it deserves its own change.

The test asserts both arms from one timings object — suppressed on a local
turn, present on a remote one. A screenshot could not carry this: the claim is
an absence, and an absence in a capture cannot be told apart from a stale build
or a turn that never populated timings.

"Reset to server default" no longer offers a value saving would reject
(76ce3ec7).

--top-k 0 is the ordinary way to disable top-k and --repeat-last-n -1 is an
ordinary setting; this app's ranges start at 1 and 0. The chip rendered, pressed,
and then Save aborted with an alert naming a parameter the user never typed, with
no route back but discarding the draft. An out-of-range default is now withheld
entirely — claiming it as "server default" would be equally wrong.

Both fixes were checked by reverting them: exactly two tests fail, one per fix,
and pass again when restored.

Gates: 274 suites, 4448 passed, 2 skipped. Typecheck and lint clean.

The remaining review findings are unaddressed and still stand — in particular the
server defaults presented on Pal and preset sheets that configure a different
model, and the unbounded persisted build_info / model_alias strings.

Visual evidence is still not posted; that is an environment blocker, not an
authoring one.

Generated by PocketPal Dev Team

@pocketpal-dev-team pocketpal-dev-team Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — formalising the verdict already reached. Findings and
evidence are in the full review;
this review does not restate them.

Current state, not the state at the time of that review:

  • 1 blocking — no visual evidence posted. Environment, not authoring: the
    upload tool has no session token. The captures that exist cover the remote
    surfaces only, as the PR body now states.
  • 2 of 7 concerns closed in round 2
    — the footer parts now gate on origin, and the reset no longer offers a
    default that saving would reject. Both checked by reverting them.
  • 5 concerns open. One (server defaults presented on sheets configuring a
    different model) is deliberately deferred to a follow-up. Four are
    unaddressed: the volatile tier is structurally assignable into both persisted
    tiers; build_info / model_alias are unbounded persisted strings against a
    6 MB Android store ceiling; the new probe coalescing regresses two recovery
    paths; and PROPS_SCALAR_FIELDS reintroduces the drift its neighbour exists
    to prevent.

No code blocker. Eight role reviewers plus an adversarial refutation pass, none
of them the implementer.

Generated by PocketPal Dev Team

@pocketpal-dev-team

Copy link
Copy Markdown
Contributor

Visual evidence — remote surfaces

sampler-server-defaults-chat.png
sampler-server-defaults-pal.png
remote-turn-footer-cold.png
remote-turn-footer-warm.png

Generated by PocketPal Dev Team

@pocketpal-dev-team

pocketpal-dev-team Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Verdict status — kept current as rounds close.

Visual evidence is posted
— four captures, remote surfaces, scope and provenance stated in the PR body.

(Edited twice. An earlier revision said the upload "was never actually blocked"
and blamed the posting helper alone; both were wrong — two independent faults
were live, and the failed attempts predate both fixes by about 4.7 hours. A
later revision listed five open concerns, which four rounds have since reduced.
Corrections left visible rather than silently applied.)

Open: 1 of the original 7 concerns.

Server sampler defaults are shown on, and reset into, Pal and preset sheets that
configure a different model than the one bound. Deliberately deferred to a
follow-up rather than widened into this PR; the agreed shape is to hide the
affordance when the bound model differs from the one being configured.

Closed: 6. The footer parts gate on origin rather than presence; the reset
withholds a default that saving would reject; the volatile tier is no longer
assignable into a persisted one; unbounded server strings are no longer
persisted; probe coalescing no longer answers a repointed server or survives
backgrounding; and the no-op write check derives its field set instead of
restating it.

The CHANGES_REQUESTED review stands until a human clears it — this note tracks
the count, it does not clear the verdict.

Generated by PocketPal Dev Team

… one

Every field of both persisted tiers is optional, so without a discriminant the
presence record was structurally assignable into either, and a mistyped write
would have persisted a reading of the moment as a settled fact.
A 4MB build_info produced an 8.4MB persisted blob against Android's 6MB store
ceiling, and the store is written as one record, so servers and the privacy
acknowledgement go down with it. Nothing read either field.

Deleting them buys an invariant a cap does not: every persisted field is now a
number, a boolean, or the url the user typed. No server-supplied free text is
stored at all, which is checkable by inspection and needs no constant. A cap
would also have left the aggregate open, since the map is keyed per model and
the model list is the server's to choose.

The parse helper went with them; they were its only callers.
The in-flight map was keyed without the url, so a refresh issued after a server
edit joined the pre-edit request and reported its result for the new backend.

It also outlived backgrounding. The foreground reprobe exists because the first
probe is the request that raises the local-network prompt, so a grant always
arrives after it has failed; joining that request returns the failure it was
meant to replace.

The no-op write check now derives its scalar set from the field list rather
than restating it, so a field added to one cannot be skipped by the other.
@pocketpal-dev-team

Copy link
Copy Markdown
Contributor

Four review concerns fixed

Each was a defect this change introduced, rather than an improvement it declined
to make — which is why they were worth a round rather than a follow-up.

Unbounded server strings are no longer persisted (5dfadd8a). A 4 MB
build_info was measured producing an 8.4 MB persisted blob against Android's
6 MB store ceiling; the store is written as one record, so servers and the
privacy acknowledgement go down with it. Nothing read either field.

They were deleted rather than capped, on an independent reading. Deletion buys
an invariant a cap does not: every persisted field is now a number, a boolean,
or the url the user typed — no server-supplied free text is stored at all.

That is checkable by inspection and needs no constant to justify. A cap would
also have left the aggregate open, since the map is keyed per model and the
model list is the server's to choose. The parse helper went with them; those
were its only callers.

The volatile tier can no longer be assigned into a persisted one
(c37e5f84). Every field of both persisted records is optional, so without a
discriminant the presence record was structurally assignable into either — and
a mistyped write would have stored a reading of the moment as a settled fact.

Probe coalescing no longer answers the wrong question (36108e9b). The
in-flight map was keyed without the url, so a refresh issued after a server edit
joined the pre-edit request and reported its result for the new backend. It also
outlived backgrounding, which matters because the foreground reprobe exists
precisely because the first probe is the request that raises the local-network
prompt — a grant always arrives after it has failed, and joining that request
returns the failure it was meant to replace.

The same commit derives the no-op write check's scalar set from the field list
instead of restating it, so a field added to one can no longer be skipped by the
other — which is what the neighbouring comment already claimed.

Checks

274 suites, 4450 passed, 2 skipped. Typecheck and lint clean.

Each fix was checked by reverting it. Re-adding the string parse fails the test
asserting the fields are absent from the parsed result while present on the
wire. Removing the discriminant makes a presence → caps assignment compile
that currently errors — both directions run, so the type is doing the work.
Reverting either half of the coalescing fix fails its named test.

The one exception is stated rather than implied: the derived scalar set has no
test, because the fix removes the failure's representation rather than detecting
it. There is nothing left to assert.

No visual change, so the posted captures are unaffected.

Generated by PocketPal Dev Team

@pocketpal-dev-team

Copy link
Copy Markdown
Contributor

Scope of the independent review, stated because it is narrower than it reads.

The review above was conducted against e9bee6c2. Five commits have landed
since — bc8ec498, 76ce3ec7, c37e5f84, 5dfadd8a, 36108e9b.

Each was written in response to a finding from that review, each was checked by
reverting it and confirming the named test fails, the full suite is green, and
the field deletion in 5dfadd8a was decided on an independent reading rather
than my own judgement. But none of the five has had an independent review pass
of its own.

"Independent review completed" is a claim about a specific tree. It is true of
e9bee6c2 and it is not a statement about this PR's current head. Whether the
delta warrants its own pass is a reviewer's call, not mine to waive.

Generated by PocketPal Dev Team

Clearing the in-flight map leaves earlier probes pending, so deleting by key
let a late settle evict the replacement registered under that key — defeating
coalescing for exactly the reprobe the clear exists to allow. On iOS that is
the likely ordering rather than a corner: the abort timer is frozen while
suspended and fires on resume.

The merge now carries forward only declared fields. Spreading the hydrated
entry meant a field removed from the schema was copied forward for the life of
that entry, so a removal could never take effect.

A scalar set derived from the field list stops a new field being skipped, but
not a new object-valued one being compared by identity — which, against a body
parsed fresh each probe, is never equal and rewrites the map every time. That
is now a compile error.
Without a remote turn snapshot the parts were withheld by the origin gate, not
by the server's silence, so the test passed whether the gate existed or not and
nothing covered a remote turn on a build that omits prompt speed.
@pocketpal-dev-team

Copy link
Copy Markdown
Contributor

Delta review of the fix commits, and what it found

The review posted earlier covered e9bee6c2. Five commits landed after it, in
response to it. Those five have now had their own independent pass — two
reviewers, scoped to that range, with the base and the deferred concern fenced
out.

It found a bug in one of the fixes, which is the outcome that justified
running it. probesInFlight.delete(key) deleted by key rather than by identity,
so a probe cleared while still pending could settle later and evict the
replacement registered under that key — defeating coalescing for exactly the
reprobe the clear exists to enable. Both reviewers found it independently and
both demonstrated three requests where two were expected. On iOS the triggering
order is the likely one rather than a corner, because the abort timer is frozen
while the app is suspended and fires on resume.

A per-commit check could not have caught it. Each of those commits was verified
by reverting it and confirming a named test fails; this is an interaction
between the two halves of one commit, which that method is structurally blind
to.

Three smaller findings, all fixed:

  • The merge spread the hydrated prior entry unfiltered, so a field removed from
    the schema was carried forward for the life of that entry — meaning removal
    from this schema could not take effect at all
    . It also made an architecture
    invariant false for already-stored entries; the doc now says what is written
    rather than what is stored, and names when a stored entry catches up.
  • Deriving the scalar comparison set stopped a new field being skipped but not
    a new object-valued one being compared by identity — never equal against a
    freshly parsed body, so every probe would rewrite and re-persist the map.
    Silent-skip traded for silent-churn. That is now a compile error.
  • A footer test named for absence no longer exercised absence: the origin gate
    withheld the parts, so it passed whether the gate existed or not, and nothing
    covered a remote turn on a build that omits prompt speed.

Two tests also falsified by timeout rather than by assertion. A hang reads as
environment flake, so they were restructured to fail on the assertion — now 5 ms
and 1 ms, printing expected versus received.

Every fix carries its own check: reverting the identity delete reproduces the
reviewers' three-versus-two exactly, and adding an object-valued field to the
scalar set is now a compile error.

274 suites, 4451 passed. No visual change, so the posted captures are unaffected.

Generated by PocketPal Dev Team

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant