Premise
PR #12112 landed proactive lms load <model> invocation on orchestrator-managed lms task spawn via ai/services/graph/ProviderReadinessHelper.mjs:loadLmsModel. The helper invokes execFile('lms', ['load', model], ...) WITHOUT passing --context-length. This is the L4 validation gap surfaced in PR #12112's ## Post-Merge Validation and during the actual post-merge Sandman run (2026-05-27):
Empirical anchor (post-PR-#12115-merge orchestrator restart + ai:run-sandman):
- LM Studio with
gemma-4-31b-it loaded via lms load (default modelfile context ~4K-8K)
- Sandman REM cycle: all 10 sessions returned silent empty-response → PR #12113 detection correctly fired
context-overflow friction for all 10 sessions
- Direct V-B-A:
curl -sN /v1/chat/completions with tiny prompts to the same LM Studio endpoint returns content cleanly → confirms LM Studio + model alive
- Root cause: loaded-context-cap < composed-prompt-size; even 18K-char (~4.5K-token) sessions overflow the modelfile-default context window
Operator manually ran lms load gemma-4-31b-it --context-length 262144 to recover — that command is exactly what loadLmsModel SHOULD have done automatically per the substrate intent of #12090.
Substrate shape
The neo-side aiConfig.localModels.chat.contextLimitTokens (PR #12115's role-keyed split — default 262144) is the operator's declared chat-context-window threshold. The lms-side loaded-model context-cap MUST be ≥ this value or every chat invocation silently fails the loaded-cap pre-check. These two layers are currently INDEPENDENT.
loadLmsModel is the substrate boundary where they SHOULD be tied together: the orchestrator already knows the neo-side threshold (localModels.chat.contextLimitTokens flows through Orchestrator.lmsModels getter + TaskDefinitions postSpawn hook), so it can pass it directly to lms load --context-length <N>.
Prescription
ProviderReadinessHelper.loadLmsModel: accept new contextLength param; when present, append '--context-length', String(contextLength) to the execFile args.
ProviderReadinessHelper.ensureLmsModelsLoaded: accept contextLength param; thread it through to each loadLmsModel(model, {contextLength}) call.
TaskDefinitions.mjs lms task postSpawn hook: pass contextLength: aiConfig.localModels.chat.contextLimitTokens (chat-role since LM Studio's loaded gemma is the chat consumer; embedding context-cap for the embedding model would be a parallel threading).
- Sibling defense — embedding context: pass
contextLength: aiConfig.localModels.embedding.contextLimitTokens for the embedding-model load invocation in the same ensureLmsModelsLoaded orchestration (per PR #12112's design which loads BOTH chat + embedding models).
Contract Ledger
| Surface |
Before |
After |
Consumer |
loadLmsModel(model, options) |
execFile('lms', ['load', model], ...) — modelfile-default context (~4K-8K for gemma-4-31b-it) |
execFile('lms', ['load', model, '--context-length', String(contextLength)], ...) when contextLength provided |
Orchestrator lms task postSpawn hook |
ensureLmsModelsLoaded(options) |
No context-length awareness |
New contextLength param (or per-model object keyed by role) |
TaskDefinitions chat + embedding model load invocations |
| Loaded-cap / neo-threshold alignment |
Independent — operator must manually align |
Orchestrator-owned via threading from aiConfig.localModels.{chat,embedding}.contextLimitTokens |
Eliminates the L4 silent-context-mismatch failure mode |
Acceptance Criteria
Related
- Parent: Epic #12101 (greenfield aiConfig substrate redesign —
localModels role-keyed split lives here)
- Source: PR #12112 (#12090 lms proactive load — established the postSpawn hook + ensureLmsModelsLoaded substrate; this ticket completes the threading)
- Sibling: #12114 (role-keyed context-limits — established the localModels.{chat,embedding}.contextLimitTokens source-of-truth this ticket threads from)
- Sibling: #12116 (ConsumerFrictionHelper loud-fail — same defense-in-depth class for the consumer-side telemetry)
- Empirical anchor: post-merge 2026-05-27 Sandman validation surfaced 10 silent-empty sessions before operator manually
lms load --context-length 262144 recovered
Discipline anchor
PR #12112 review cycle-1 flagged the loadLmsModel no-timeout concern as a depth-floor challenge ("Not a merge-blocker — the failure mode is 'boot stalls visibly' rather than 'boots into wrong state'"). The no-context-length is the same class of L4 gap, just with a DIFFERENT failure mode — "boots into context-mismatched state with silent downstream failure" rather than "boots into wrong state". Both are post-merge-validation-grade gaps that the operator surfaced via dogfooding. Closing this ticket completes the L4 lms-proactive-load substrate intent of #12090.
Premise
PR #12112 landed proactive
lms load <model>invocation on orchestrator-managed lms task spawn viaai/services/graph/ProviderReadinessHelper.mjs:loadLmsModel. The helper invokesexecFile('lms', ['load', model], ...)WITHOUT passing--context-length. This is the L4 validation gap surfaced in PR #12112's## Post-Merge Validationand during the actual post-merge Sandman run (2026-05-27):Empirical anchor (post-PR-#12115-merge orchestrator restart + ai:run-sandman):
gemma-4-31b-itloaded vialms load(default modelfile context ~4K-8K)context-overflowfriction for all 10 sessionscurl -sN /v1/chat/completionswith tiny prompts to the same LM Studio endpoint returns content cleanly → confirms LM Studio + model aliveOperator manually ran
lms load gemma-4-31b-it --context-length 262144to recover — that command is exactly whatloadLmsModelSHOULD have done automatically per the substrate intent of #12090.Substrate shape
The neo-side
aiConfig.localModels.chat.contextLimitTokens(PR #12115's role-keyed split — default 262144) is the operator's declared chat-context-window threshold. The lms-side loaded-model context-cap MUST be ≥ this value or every chat invocation silently fails the loaded-cap pre-check. These two layers are currently INDEPENDENT.loadLmsModelis the substrate boundary where they SHOULD be tied together: the orchestrator already knows the neo-side threshold (localModels.chat.contextLimitTokensflows throughOrchestrator.lmsModelsgetter +TaskDefinitionspostSpawn hook), so it can pass it directly tolms load --context-length <N>.Prescription
ProviderReadinessHelper.loadLmsModel: accept newcontextLengthparam; when present, append'--context-length', String(contextLength)to the execFile args.ProviderReadinessHelper.ensureLmsModelsLoaded: acceptcontextLengthparam; thread it through to eachloadLmsModel(model, {contextLength})call.TaskDefinitions.mjslms taskpostSpawnhook: passcontextLength: aiConfig.localModels.chat.contextLimitTokens(chat-role since LM Studio's loaded gemma is the chat consumer; embedding context-cap for the embedding model would be a parallel threading).contextLength: aiConfig.localModels.embedding.contextLimitTokensfor the embedding-model load invocation in the sameensureLmsModelsLoadedorchestration (per PR #12112's design which loads BOTH chat + embedding models).Contract Ledger
loadLmsModel(model, options)execFile('lms', ['load', model], ...)— modelfile-default context (~4K-8K for gemma-4-31b-it)execFile('lms', ['load', model, '--context-length', String(contextLength)], ...)whencontextLengthprovidedpostSpawnhookensureLmsModelsLoaded(options)contextLengthparam (or per-model object keyed by role)aiConfig.localModels.{chat,embedding}.contextLimitTokensAcceptance Criteria
loadLmsModelacceptscontextLengthoption; when provided, appends'--context-length', String(contextLength)to execFile args; when omitted (legacy callers), behavior unchangedensureLmsModelsLoadedaccepts per-model context-length config (or a single value if treating chat+embedding uniformly); passes to eachloadLmsModelinvocationTaskDefinitions.mjslms taskpostSpawnhook readsaiConfig.localModels.chat.contextLimitTokensfor the chat model load andaiConfig.localModels.embedding.contextLimitTokensfor the embedding model loadloadLmsModelwith contextLength asserts execFile args contain'--context-length'+ numeric value; ensureLmsModelsLoaded threading covered via stubbed loadModel injectionlmsEnabled=true+ neo's chat context-limit 262144 loads gemma chat model with --context-length 262144 → Sandman REM cycle no longer silent-empty on context-mismatch groundsRelated
localModelsrole-keyed split lives here)lms load --context-length 262144recoveredDiscipline anchor
PR #12112 review cycle-1 flagged the loadLmsModel no-timeout concern as a depth-floor challenge ("Not a merge-blocker — the failure mode is 'boot stalls visibly' rather than 'boots into wrong state'"). The no-context-length is the same class of L4 gap, just with a DIFFERENT failure mode — "boots into context-mismatched state with silent downstream failure" rather than "boots into wrong state". Both are post-merge-validation-grade gaps that the operator surfaced via dogfooding. Closing this ticket completes the L4 lms-proactive-load substrate intent of #12090.