fix(cli): model set/picker preserve hand-set modalities on re-set (#1127)#1610
Merged
Conversation
Aaronontheweb
added a commit
that referenced
this pull request
Jul 10, 2026
…bbers it Hardens the non-destructive `model set`/picker rewrite (#1127) against every issue surfaced reviewing #1610, and closes the loop on modality overrides. ContextWindow and modalities are documented to "take precedence over provider-reported capability detection", so they are now treated as operator-owned overrides with a single precedence rule: explicit operator input > existing stored value > probe. A fresh probe seeds a first-time set or a model switch but never overwrites a value already on disk. Changes: 1. ContextWindow clamp preserved on same-model re-set. WriteRole takes the explicit --context-window and the probe default separately; the old callers collapsed them (`contextWindow ?? discovered`), so probe/picker paths always passed a non-null value and the operator's clamp was overwritten on every re-selection. 2. Modalities are no longer silently overwritten by discovery. Previously a probe that reported modalities replaced a stored override (the #1127 loss's twin); now the stored value wins, matching the field's "manual override bypasses detection" contract. 3. Operators can change/remove those overrides. Since discovery no longer edits them, add `--input-modalities`, `--output-modalities`, and `--clear-modalities` to `model set`. Explicit set replaces the stored value; clear removes it (runtime detection resolves). Supplying any of them (like --context-window) skips the probe as manual configuration. 4. Corrupt/legacy existing entry no longer aborts the command. ReadSameModelEntry guards the deserialize (catch JsonException): an unreadable entry (e.g. an unrecognized modality enum string) degrades to "nothing to preserve" and the command overwrites/repairs it. 5. No false-match on ModelReference defaults. Provider/ModelId default to the stock local-ollama model, so an entry omitting either key deserialized to that default and would false-match a re-set of the stock model; preservation now requires both keys. 6. Provenance not downgraded. A same-model re-set that did not re-resolve the ID (no probe → Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery updates it. Tests: ModelEntryWriter unit coverage for each precedence path (clamp-over-probe, probe- does-not-override-existing-modalities, explicit set, clear-over-probe, first-time seeding, default-model false-match, corrupt-entry overwrite, provenance preserve/update) plus CLI end-to-end coverage for the new flags. Full CLI suite green; slopwatch clean; model-manager smoke tape passes.
Comment on lines
+148
to
+152
| foreach (var property in element.EnumerateObject()) | ||
| { | ||
| if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) | ||
| return true; | ||
| } |
Comment on lines
+156
to
+160
| foreach (var key in dict.Keys) | ||
| { | ||
| if (string.Equals(key, propertyName, StringComparison.OrdinalIgnoreCase)) | ||
| return true; | ||
| } |
Collaborator
Author
|
📝 Docs follow-up filed for the docs website: netclaw-dev/netclaw-website#83 — covers the |
Aaronontheweb
added a commit
that referenced
this pull request
Jul 10, 2026
) Addresses code-review findings on the non-destructive model-set change: - probe gate: only --context-window short-circuits the probe; a modality flag no longer skips model-existence validation and context-window discovery - preservation read: a corrupt modality enum string no longer discards a valid operator-owned ContextWindow (field-tolerant recovery) - arg parsing: missing flag values and unknown args fail loudly instead of being silently dropped - cleared modality is now sticky: discovery is hands-off once a same-model entry exists, so a later probe cannot resurrect a --clear-modalities removal - TryParseModalities rejects raw numeric strings (named flags only) - provenance: preserve a prior discovered origin on any non-Live re-set (was only guarding Manual) - new --clear-context-window flag to force window re-detection (symmetry with --clear-modalities) Also hardens LoadModelSelection: a corrupt/legacy config no longer crashes `model set` (repairs it) or `model list` (reports it cleanly) or the TUI. Generalizes ModalityOverride into a shared ValueOverride<T> tri-state. Updates netclaw-operations skill (providers.md). Docs website tracked in netclaw-dev/netclaw-website#83.
Comment on lines
+197
to
+205
| foreach (var property in root.EnumerateObject()) | ||
| { | ||
| if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase) | ||
| && property.Value.ValueKind == JsonValueKind.String) | ||
| { | ||
| value = property.Value.GetString()!; | ||
| return true; | ||
| } | ||
| } |
Comment on lines
+213
to
+225
| foreach (var property in root.EnumerateObject()) | ||
| { | ||
| if (!string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) | ||
| continue; | ||
|
|
||
| // Web-default reads coerce numeric strings, so accept both a JSON number and a | ||
| // stringified integer to match what the strict path would have parsed. | ||
| if (property.Value.ValueKind == JsonValueKind.Number && property.Value.TryGetInt32(out value)) | ||
| return true; | ||
| if (property.Value.ValueKind == JsonValueKind.String | ||
| && int.TryParse(property.Value.GetString(), out value)) | ||
| return true; | ||
| } |
Comment on lines
+126
to
+171
| foreach (var role in new[] { "Main", "Fallback", "Compaction" }) | ||
| { | ||
| if (!modelsSection.TryGetValue(role, out var raw) || raw is null) | ||
| continue; | ||
|
|
||
| if (!RawContainsProperty(raw, nameof(ModelReference.Provider)) | ||
| || !RawContainsProperty(raw, nameof(ModelReference.ModelId))) | ||
| { | ||
| if (string.Equals(role, overwrittenRole, StringComparison.OrdinalIgnoreCase)) | ||
| continue; | ||
| throw new InvalidOperationException( | ||
| $"Models:{role} must explicitly declare Provider and ModelId before migration."); | ||
| } | ||
|
|
||
| ModelReference model; | ||
| try | ||
| { | ||
| model = ConfigFileHelper.DeserializeSection<ModelReference>(raw) | ||
| ?? throw new InvalidOperationException($"Models:{role} could not be parsed."); | ||
| } | ||
| catch (JsonException) | ||
| { | ||
| model = ReadLegacyIdentity(raw) | ||
| ?? throw new InvalidOperationException($"Models:{role} could not be repaired."); | ||
| } | ||
| var existingName = FindDefinition(definitions, model.Provider, model.ModelId); | ||
| if (existingName is not null) | ||
| { | ||
| var existing = ConfigFileHelper.DeserializeSection<ModelReference>(definitions[existingName])!; | ||
| if (!Equivalent(existing, model)) | ||
| { | ||
| throw new InvalidOperationException( | ||
| $"Legacy model roles conflict for {model.Provider}/{model.ModelId}; " + | ||
| $"align their metadata before migration."); | ||
| } | ||
|
|
||
| roles[role] = existingName; | ||
| continue; | ||
| } | ||
|
|
||
| var name = CreateDefinitionName(definitions, model.Provider, model.ModelId); | ||
| definitions[name] = BuildModelEntry( | ||
| model.Provider, model.ModelId, model.Provenance, model.ContextWindow, | ||
| model.InputModalities, model.OutputModalities); | ||
| roles[role] = name; | ||
| } |
) Re-selecting a model that is already configured wiped operator-set attributes on it. The write path rebuilt the Models[role] entry from scratch via ModelEntryWriter, which only writes modalities it was handed (a probe result). Modalities have no CLI input and can only be hand-edited, so a manual 'model set' (or a context-window tweak, or the TUI picker re-selecting the same model) passed null and silently deleted a hand-set InputModalities/OutputModalities — the concrete #1127 loss. Add ModelEntryWriter.WriteRole, a non-destructive persist: when the role already points at the same (provider, modelId), preserve the existing modalities and context window the caller did not supply; switching to a different model still starts clean (old attributes belonged to the old model). Routed 'model set' and the TUI model manager through it. Verified against the shipped binary: on stock beta 0.25.0, 'model set main <same-model> --context-window N' wipes InputModalities; with this fix the same command preserves it. No config-shape or schema change, so this is fully backwards compatible. Tests: WriteRole_SameModelWithoutModalities_PreservesHandSetModalities, WriteRole_DifferentModel_DropsPreviousModelModalities.
…bbers it Hardens the non-destructive `model set`/picker rewrite (#1127) against every issue surfaced reviewing #1610, and closes the loop on modality overrides. ContextWindow and modalities are documented to "take precedence over provider-reported capability detection", so they are now treated as operator-owned overrides with a single precedence rule: explicit operator input > existing stored value > probe. A fresh probe seeds a first-time set or a model switch but never overwrites a value already on disk. Changes: 1. ContextWindow clamp preserved on same-model re-set. WriteRole takes the explicit --context-window and the probe default separately; the old callers collapsed them (`contextWindow ?? discovered`), so probe/picker paths always passed a non-null value and the operator's clamp was overwritten on every re-selection. 2. Modalities are no longer silently overwritten by discovery. Previously a probe that reported modalities replaced a stored override (the #1127 loss's twin); now the stored value wins, matching the field's "manual override bypasses detection" contract. 3. Operators can change/remove those overrides. Since discovery no longer edits them, add `--input-modalities`, `--output-modalities`, and `--clear-modalities` to `model set`. Explicit set replaces the stored value; clear removes it (runtime detection resolves). Supplying any of them (like --context-window) skips the probe as manual configuration. 4. Corrupt/legacy existing entry no longer aborts the command. ReadSameModelEntry guards the deserialize (catch JsonException): an unreadable entry (e.g. an unrecognized modality enum string) degrades to "nothing to preserve" and the command overwrites/repairs it. 5. No false-match on ModelReference defaults. Provider/ModelId default to the stock local-ollama model, so an entry omitting either key deserialized to that default and would false-match a re-set of the stock model; preservation now requires both keys. 6. Provenance not downgraded. A same-model re-set that did not re-resolve the ID (no probe → Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery updates it. Tests: ModelEntryWriter unit coverage for each precedence path (clamp-over-probe, probe- does-not-override-existing-modalities, explicit set, clear-over-probe, first-time seeding, default-model false-match, corrupt-entry overwrite, provenance preserve/update) plus CLI end-to-end coverage for the new flags. Full CLI suite green; slopwatch clean; model-manager smoke tape passes.
) Addresses code-review findings on the non-destructive model-set change: - probe gate: only --context-window short-circuits the probe; a modality flag no longer skips model-existence validation and context-window discovery - preservation read: a corrupt modality enum string no longer discards a valid operator-owned ContextWindow (field-tolerant recovery) - arg parsing: missing flag values and unknown args fail loudly instead of being silently dropped - cleared modality is now sticky: discovery is hands-off once a same-model entry exists, so a later probe cannot resurrect a --clear-modalities removal - TryParseModalities rejects raw numeric strings (named flags only) - provenance: preserve a prior discovered origin on any non-Live re-set (was only guarding Manual) - new --clear-context-window flag to force window re-detection (symmetry with --clear-modalities) Also hardens LoadModelSelection: a corrupt/legacy config no longer crashes `model set` (repairs it) or `model list` (reports it cleanly) or the TUI. Generalizes ModalityOverride into a shared ValueOverride<T> tri-state. Updates netclaw-operations skill (providers.md). Docs website tracked in netclaw-dev/netclaw-website#83.
Aaronontheweb
force-pushed
the
fix/model-set-non-destructive
branch
from
July 11, 2026 19:39
551d26e to
4b105ed
Compare
Aaronontheweb
marked this pull request as ready for review
July 12, 2026 00:06
Aaronontheweb
enabled auto-merge
July 12, 2026 00:06
Aaronontheweb
added this pull request to the merge queue
Jul 12, 2026
This was referenced Jul 12, 2026
Merged
Aaronontheweb
added a commit
that referenced
this pull request
Jul 12, 2026
…pare 0.25.0-alpha.onnx.5 (#1619) * fix: support Discord DM reminders (#1609) * Refactor ModelContextProtocol versioning in props file (#1614) Updated ModelContextProtocol package versions to use a variable for versioning. Signed-off-by: Aaron Stannard <[email protected]> * fix: serialize Slack processing status updates (#1556) Co-authored-by: Aaron Stannard <[email protected]> * ci: run required checks for merge queue groups (#1617) * Bump MessagePack from 3.1.7 to 3.1.8 (#1605) --- updated-dependencies: - dependency-name: MessagePack dependency-version: 3.1.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(cli): model set/picker preserve hand-set modalities on re-set (#1127) (#1610) * fix(cli): model set/picker preserve hand-set modalities on re-set (#1127) Re-selecting a model that is already configured wiped operator-set attributes on it. The write path rebuilt the Models[role] entry from scratch via ModelEntryWriter, which only writes modalities it was handed (a probe result). Modalities have no CLI input and can only be hand-edited, so a manual 'model set' (or a context-window tweak, or the TUI picker re-selecting the same model) passed null and silently deleted a hand-set InputModalities/OutputModalities — the concrete #1127 loss. Add ModelEntryWriter.WriteRole, a non-destructive persist: when the role already points at the same (provider, modelId), preserve the existing modalities and context window the caller did not supply; switching to a different model still starts clean (old attributes belonged to the old model). Routed 'model set' and the TUI model manager through it. Verified against the shipped binary: on stock beta 0.25.0, 'model set main <same-model> --context-window N' wipes InputModalities; with this fix the same command preserves it. No config-shape or schema change, so this is fully backwards compatible. Tests: WriteRole_SameModelWithoutModalities_PreservesHandSetModalities, WriteRole_DifferentModel_DropsPreviousModelModalities. * fix(cli): make model-set metadata operator-owned; discovery never clobbers it Hardens the non-destructive `model set`/picker rewrite (#1127) against every issue surfaced reviewing #1610, and closes the loop on modality overrides. ContextWindow and modalities are documented to "take precedence over provider-reported capability detection", so they are now treated as operator-owned overrides with a single precedence rule: explicit operator input > existing stored value > probe. A fresh probe seeds a first-time set or a model switch but never overwrites a value already on disk. Changes: 1. ContextWindow clamp preserved on same-model re-set. WriteRole takes the explicit --context-window and the probe default separately; the old callers collapsed them (`contextWindow ?? discovered`), so probe/picker paths always passed a non-null value and the operator's clamp was overwritten on every re-selection. 2. Modalities are no longer silently overwritten by discovery. Previously a probe that reported modalities replaced a stored override (the #1127 loss's twin); now the stored value wins, matching the field's "manual override bypasses detection" contract. 3. Operators can change/remove those overrides. Since discovery no longer edits them, add `--input-modalities`, `--output-modalities`, and `--clear-modalities` to `model set`. Explicit set replaces the stored value; clear removes it (runtime detection resolves). Supplying any of them (like --context-window) skips the probe as manual configuration. 4. Corrupt/legacy existing entry no longer aborts the command. ReadSameModelEntry guards the deserialize (catch JsonException): an unreadable entry (e.g. an unrecognized modality enum string) degrades to "nothing to preserve" and the command overwrites/repairs it. 5. No false-match on ModelReference defaults. Provider/ModelId default to the stock local-ollama model, so an entry omitting either key deserialized to that default and would false-match a re-set of the stock model; preservation now requires both keys. 6. Provenance not downgraded. A same-model re-set that did not re-resolve the ID (no probe → Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery updates it. Tests: ModelEntryWriter unit coverage for each precedence path (clamp-over-probe, probe- does-not-override-existing-modalities, explicit set, clear-over-probe, first-time seeding, default-model false-match, corrupt-entry overwrite, provenance preserve/update) plus CLI end-to-end coverage for the new flags. Full CLI suite green; slopwatch clean; model-manager smoke tape passes. * fix(cli): harden model-set overrides + add --clear-context-window (#1610) Addresses code-review findings on the non-destructive model-set change: - probe gate: only --context-window short-circuits the probe; a modality flag no longer skips model-existence validation and context-window discovery - preservation read: a corrupt modality enum string no longer discards a valid operator-owned ContextWindow (field-tolerant recovery) - arg parsing: missing flag values and unknown args fail loudly instead of being silently dropped - cleared modality is now sticky: discovery is hands-off once a same-model entry exists, so a later probe cannot resurrect a --clear-modalities removal - TryParseModalities rejects raw numeric strings (named flags only) - provenance: preserve a prior discovered origin on any non-Live re-set (was only guarding Manual) - new --clear-context-window flag to force window re-detection (symmetry with --clear-modalities) Also hardens LoadModelSelection: a corrupt/legacy config no longer crashes `model set` (repairs it) or `model list` (reports it cleanly) or the TUI. Generalizes ModalityOverride into a shared ValueOverride<T> tri-state. Updates netclaw-operations skill (providers.md). Docs website tracked in netclaw-dev/netclaw-website#83. * feat(config): preserve model definitions across role switches * fix(config): validate named model role references * fix(cli): preserve models when editing providers * chore(deps): bump dotnet-sdk from 10.0.300 to 10.0.301 (#1381) Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 10.0.300 to 10.0.301. - [Release notes](https://github.com/dotnet/sdk/releases) - [Commits](dotnet/sdk@v10.0.300...v10.0.301) --- updated-dependencies: - dependency-name: dotnet-sdk dependency-version: 10.0.301 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(subagents): fail closed for unattended approvals (#1616) * chore(release): prepare 0.25.0-alpha.onnx.5 experimental prerelease --------- Signed-off-by: Aaron Stannard <[email protected]> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: petabridge-netclaw[bot] <289234546+petabridge-netclaw[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Aaronontheweb
added a commit
that referenced
this pull request
Jul 14, 2026
…0.25.0-alpha.onnx.6 (#1642) * fix: support Discord DM reminders (#1609) * Refactor ModelContextProtocol versioning in props file (#1614) Updated ModelContextProtocol package versions to use a variable for versioning. Signed-off-by: Aaron Stannard <[email protected]> * fix: serialize Slack processing status updates (#1556) Co-authored-by: Aaron Stannard <[email protected]> * ci: run required checks for merge queue groups (#1617) * Bump MessagePack from 3.1.7 to 3.1.8 (#1605) --- updated-dependencies: - dependency-name: MessagePack dependency-version: 3.1.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(cli): model set/picker preserve hand-set modalities on re-set (#1127) (#1610) * fix(cli): model set/picker preserve hand-set modalities on re-set (#1127) Re-selecting a model that is already configured wiped operator-set attributes on it. The write path rebuilt the Models[role] entry from scratch via ModelEntryWriter, which only writes modalities it was handed (a probe result). Modalities have no CLI input and can only be hand-edited, so a manual 'model set' (or a context-window tweak, or the TUI picker re-selecting the same model) passed null and silently deleted a hand-set InputModalities/OutputModalities — the concrete #1127 loss. Add ModelEntryWriter.WriteRole, a non-destructive persist: when the role already points at the same (provider, modelId), preserve the existing modalities and context window the caller did not supply; switching to a different model still starts clean (old attributes belonged to the old model). Routed 'model set' and the TUI model manager through it. Verified against the shipped binary: on stock beta 0.25.0, 'model set main <same-model> --context-window N' wipes InputModalities; with this fix the same command preserves it. No config-shape or schema change, so this is fully backwards compatible. Tests: WriteRole_SameModelWithoutModalities_PreservesHandSetModalities, WriteRole_DifferentModel_DropsPreviousModelModalities. * fix(cli): make model-set metadata operator-owned; discovery never clobbers it Hardens the non-destructive `model set`/picker rewrite (#1127) against every issue surfaced reviewing #1610, and closes the loop on modality overrides. ContextWindow and modalities are documented to "take precedence over provider-reported capability detection", so they are now treated as operator-owned overrides with a single precedence rule: explicit operator input > existing stored value > probe. A fresh probe seeds a first-time set or a model switch but never overwrites a value already on disk. Changes: 1. ContextWindow clamp preserved on same-model re-set. WriteRole takes the explicit --context-window and the probe default separately; the old callers collapsed them (`contextWindow ?? discovered`), so probe/picker paths always passed a non-null value and the operator's clamp was overwritten on every re-selection. 2. Modalities are no longer silently overwritten by discovery. Previously a probe that reported modalities replaced a stored override (the #1127 loss's twin); now the stored value wins, matching the field's "manual override bypasses detection" contract. 3. Operators can change/remove those overrides. Since discovery no longer edits them, add `--input-modalities`, `--output-modalities`, and `--clear-modalities` to `model set`. Explicit set replaces the stored value; clear removes it (runtime detection resolves). Supplying any of them (like --context-window) skips the probe as manual configuration. 4. Corrupt/legacy existing entry no longer aborts the command. ReadSameModelEntry guards the deserialize (catch JsonException): an unreadable entry (e.g. an unrecognized modality enum string) degrades to "nothing to preserve" and the command overwrites/repairs it. 5. No false-match on ModelReference defaults. Provider/ModelId default to the stock local-ollama model, so an entry omitting either key deserialized to that default and would false-match a re-set of the stock model; preservation now requires both keys. 6. Provenance not downgraded. A same-model re-set that did not re-resolve the ID (no probe → Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery updates it. Tests: ModelEntryWriter unit coverage for each precedence path (clamp-over-probe, probe- does-not-override-existing-modalities, explicit set, clear-over-probe, first-time seeding, default-model false-match, corrupt-entry overwrite, provenance preserve/update) plus CLI end-to-end coverage for the new flags. Full CLI suite green; slopwatch clean; model-manager smoke tape passes. * fix(cli): harden model-set overrides + add --clear-context-window (#1610) Addresses code-review findings on the non-destructive model-set change: - probe gate: only --context-window short-circuits the probe; a modality flag no longer skips model-existence validation and context-window discovery - preservation read: a corrupt modality enum string no longer discards a valid operator-owned ContextWindow (field-tolerant recovery) - arg parsing: missing flag values and unknown args fail loudly instead of being silently dropped - cleared modality is now sticky: discovery is hands-off once a same-model entry exists, so a later probe cannot resurrect a --clear-modalities removal - TryParseModalities rejects raw numeric strings (named flags only) - provenance: preserve a prior discovered origin on any non-Live re-set (was only guarding Manual) - new --clear-context-window flag to force window re-detection (symmetry with --clear-modalities) Also hardens LoadModelSelection: a corrupt/legacy config no longer crashes `model set` (repairs it) or `model list` (reports it cleanly) or the TUI. Generalizes ModalityOverride into a shared ValueOverride<T> tri-state. Updates netclaw-operations skill (providers.md). Docs website tracked in netclaw-dev/netclaw-website#83. * feat(config): preserve model definitions across role switches * fix(config): validate named model role references * fix(cli): preserve models when editing providers * chore(deps): bump dotnet-sdk from 10.0.300 to 10.0.301 (#1381) Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 10.0.300 to 10.0.301. - [Release notes](https://github.com/dotnet/sdk/releases) - [Commits](dotnet/sdk@v10.0.300...v10.0.301) --- updated-dependencies: - dependency-name: dotnet-sdk dependency-version: 10.0.301 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(subagents): fail closed for unattended approvals (#1616) * Release 0.25.0-beta.3: update release notes and version metadata (#1618) * Add user-written `AGENTS.md` for application-specific agent guidelines (#1622) * Add deployment agent mission playbook * Keep identity routing in embedded guidance * Evaluate embedded identity routing * Prioritize specialized subagent guidance * test: skip SearXNG container test on Windows (#1625) * Use logical skill access and authoritative inventory refresh (#1634) * feat(skills): use logical skill access * docs(evals): restore README * test(skills): use root-preserving path joins * Preserve Git working context across sessions and subagents (#1630) * feat: preserve git context across subagents * fix: make subagent git context deterministic * test: make fixture path intent explicit * Stabilize config search screenshots (#1635) * Simplify STDIO MCP process ownership (#1636) * chore: bump Netclaw.SkillClient from 0.4.0-beta.4 to 0.4.0 stable (#1638) * fix(memory): stop curation dedup from overwriting existing documents (#1637) When a curation Create decision landed on an anchor that already had a document, both batch appliers reused the existing document_id, and the ON CONFLICT DO UPDATE overwrote that document's title, body, and classification with the new proposal. The old content was lost; there is no history table to recover it from. Now a Create collision appends the new content below a dated separator and keeps the existing title, boundary, audience, and sensitivity. If the incoming content is already present verbatim, the write is skipped. Consolidate decisions carry an explicit target document id, so they keep replacing near-duplicates as designed. Update decisions and no-collision inserts are unchanged. * Release 0.25.0-beta.4: update release notes and version metadata (#1640) --------- Signed-off-by: Aaron Stannard <[email protected]> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: petabridge-netclaw[bot] <289234546+petabridge-netclaw[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Re-selecting a model that is already configured silently wiped operator-set attributes
on it — the concrete #1127 loss. The write path rebuilt the
Models[role]entry fromscratch via
ModelEntryWriter, which only wrote what it was handed (a probe result), so amanual
netclaw model set, a context-window tweak, or the TUI picker re-selecting the samemodel passed
nulland deleted a hand-setInputModalities/OutputModalities.The fix establishes a single principle:
ContextWindowand the modalities areoperator-owned overrides — both are documented to "take precedence over provider-reported
capability detection" — so provider discovery may seed them but must never clobber
them.
ModelEntryWriter.WriteRoleis the non-destructive persist that enforces oneprecedence rule everywhere: explicit operator input > existing stored value > probe.
model setand the TUI model manager route through it; the init wizard is unchanged (freshconfig, nothing to clobber).
No config-shape or schema change, so this is fully backwards compatible — existing
configs and older daemons read it exactly as before.
Commits
1.
b8b49d10— non-destructiveWriteRole. When the role already points at the same(provider, modelId), preserve the modalities and context window the caller didn't supply;switching to a different model still starts clean (the old attributes belonged to the old
model).
2.
d38b1586— make the preservation robust and the overrides editable. Review of thefirst commit surfaced that "preserve when the caller passed null" was defeated on exactly the
paths that matter (probe/picker always pass a discovered value), plus several edge cases:
explicit
--context-windowand the probe default (contextWindow ?? discovered), so theprobe/picker paths always passed a non-null value and the operator's clamp was overwritten
on every re-selection. The two are now passed separately.
modalities used to replace a stored override (the Config: should store model properties (modalities, context length) separately from which models are main / fallback #1127 loss's twin); now the stored value
wins, matching the field's "manual override bypasses detection" contract.
model setgains--input-modalities,--output-modalities, and--clear-modalities.Explicit set replaces the stored value; clear removes it (runtime detection resolves).
Supplying any of them (like
--context-window) skips the probe as manual configuration.unrecognized modality enum string) is degraded to "nothing to preserve" and overwritten,
instead of throwing
JsonExceptionout of a write the operator requested.ModelReferencedefaults.Provider/ModelIddefault to the stocklocal-ollama model, so an entry omitting either key deserialized to that default and would
false-match a re-set of the stock model; preservation now requires both keys present.
→ Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery
updates it.
Reproduced and verified against the shipped binary
Using a throwaway containerized stock instance (isolated volume, no host config touched):
model set main <same-model> --context-window 262144InputModalities0.25.0Text, ImagepreservedScope
Fixes re-setting / tweaking an already-configured model (and the TUI re-selecting the same
model), and makes modality overrides first-class — settable and clearable from the CLI. It
does not address switch to a different model and back preserving the old model's
overrides — inline config only stores the current role's model. That "properties survive
across model switches" behavior is the named-model registry follow-up (#1127 structural
change); this fix is a prerequisite it needs anyway.
Test plan
ModelEntryWriterprecedence matrix — clamp-over-probe,probe-does-not-override-existing-modalities, explicit set, clear-over-probe, first-time
seeding, default-model false-match, corrupt-entry overwrite, provenance preserve/update
ModelCommandTests):--input-modalitieswrite,--clear-modalitiesremoval, invalid-value rejection, same-model re-set preserves existing modalities
dotnet slopwatch analyze— 0 issuesAdd-FileHeaders.ps1 -Verify— cleanrun-smoke.sh model-manager— native model-picker tape passesrun-smoke.sh light(22 tapes, 9 scenarios)0.25.0(wipes) vs this branch (preserves)close #1127.