Repository navigation
[Bug]: DSH integration refuses to apply when llm-pi-ai.providers is empty ("cannot patch without risking unrelated comments or formatting") #4260
Description
Client or integration
OpenCodex dashboard
Area
Other
Summary
Enabling the DSH integration from the dashboard never applies. The panel shows this refusal, and the file is left untouched:
无法安全地更改配置: C:\Users\<user>\.dsh\settings.yaml uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so it was left alone
The CLI agrees, and 恢复中心 stays empty, so nothing was written and no snapshot exists:
clientId: dsh
state: absent
installed: true
configPath: $DSH_HOME\settings.yaml
snapshotCount: 0
retentionDegraded: false
The trigger is an empty llm-pi-ai.providers container. The source-preserving patcher in src/integrations/omp-yaml-source.ts requires every segment of the path ["llm-pi-ai", "providers", "opencodex"] to already be a non-empty plain block map. When llm-pi-ai.providers exists but is empty, locatePath returns null instead of a missing-leaf insertion point, patchYamlFragmentSource returns null, and src/integrations/writer.ts turns that into the "left alone" refusal.
What I expected instead: an empty container should be an ordinary insertion case, exactly like an absent llm-pi-ai key already is. Both are states a user reaches without hand-editing YAML.
Reproduction
Minimal $DSH_HOME/settings.yaml:
llm-pi-ai:
providers: {}
ui-theme:
preference: systemocx start- Open the dashboard, select DSH on the integration page, and toggle it on.
- Observe the refusal box. Nothing is written, and 恢复中心 registers no entry.
ocx integration client status --client dshreportsstate: absentandsnapshotCount: 0.
I also drove the shipped patcher directly, calling patchYamlFragmentSource from src/integrations/omp-yaml-source.ts with the real settings file, the path above, and a small generated provider block. Five cases:
A) providers: {} -> null (refused)
B) providers: -> null (refused) (empty block key; parses as null)
C) providers: with one sibling -> patched OK
D) providers: with opencodex -> patched OK
E) llm-pi-ai key absent entirely -> patched OK
Case A fails because isPlainBlockKey does not accept the providers: {} line, while readPath still finds the key defined, so locatePath returns null rather than falling through to the missing-key branch. Case B reaches the isPlainRecord(readPath(parsed, prefix)) guard, where an empty block key parses as null and the guard rejects it.
The empty shape is not exotic. It is what the file holds once the last provider entry is removed, and an integration that only applies when an unrelated provider already exists cannot be enabled on a clean profile.
Version
2.50.0
Operating system
Windows 11 Pro (10.0.26200)
Provider and model
Not provider-specific. Reproduced with no providers configured at all.
Logs or error output
PS> ocx integration client status --client dsh
clientId: dsh
state: absent
installed: true
configPath: $DSH_HOME\settings.yaml
snapshotCount: 0
retentionDegraded: falseDashboard refusal: 无法安全地更改配置: $DSH_HOME\settings.yaml uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so it was left alone
Redacted configuration
llm-pi-ai:
providers: {}Suggested fix
Let locatePath treat an empty container as a missing leaf:
- accept a plain block key whose value is an empty inline collection (
providers: {},providers: []) as a match, and - allow
readPath(parsed, prefix)to be null or an empty record when the matched source line is a plain key with no block children, then insert the child atparentIndent + 2.
upsertSource already has the insertion branch for this case, so the change stays confined to how the path is located.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
a1980881039 commentedon Sep 11, 2026
Second shape of the same root cause: flow-style containers also refuse
Still on 2.50.0, same machine. After the provider list was rewritten by DSH itself,
llm-pi-ai.providersis no longer empty, but it is rendered in flow style instead of a plain block map:Toggling the integration against this document also refuses. The file is untouched and 恢复中心 stays empty. Driving the shipped patcher directly shows it fails on a different step than the empty-container case:
Here the walk does get past
llm-pi-aiandproviders, becauseproviders:is a plain block key with no inline value, andcontainerEndscans to the end of the block rather than stopping at the}. SolocatePathreturns a missing-leaf entry instead of null, andupsertSourcerenders an insertion. The rejection then happens at the final re-parse inpatchYamlFragmentSource: the block-style insertion lands against a flow mapping, so the reparsed document no longer matches the expected document,semanticallyMatchesreturns false, and the result is null.In the first (empty-container) case the walk bails out early with
locatePath: null. In this one it produces bytes and the bytes are rejected. Same refusal box, same untouched file, different failing step.Why this case matters more than the empty one
This shape is emitted by DSH itself, not reached by hand editing. A user who manages their providers through DSH ends up with a file that the integration can never apply to, and the only signal is "uses YAML source opencodex cannot patch without risking unrelated comments or formatting", which points at a comment or a formatting quirk that is not there. The real cause is a container style the patcher does not accept.
Two things would help:
locatePath/upsertSource, orThe diagnostic names are taken from
src/integrations/omp-yaml-source.tsat the v2.50.0 tag; the reproduction harness calledpatchYamlFragmentSourcewith["llm-pi-ai", "providers", "opencodex"]and a generated provider block, identical to the empty-container reproduction above.lidge-jun commentedon Sep 11, 2026
리뷰 · 우선순위 68 / 80
설명
이 이슈는 Integrations → DSH를 대시보드에서 켜도 설정 파일이 한 글자도 안 바뀌고, 중국어/영문 거절 상자만 뜨는 버그입니다. 메시지 뜻은 「이 YAML을 안전하게 고칠 수 없어서 그냥 두었다」인데, 실제로는 주석이나 이상한 포맷이 아니라
llm-pi-ai.providers가 비어 있거나(flow/{}) DSH가 다시 쓴 flow-style 맵일 때 소스 보존 패처가 스스로 손을 떼는 경우입니다. 보고 버전은 2.50.0이고, CURRENTdevHEAD18e553a52(package.json 2.51.0, 팁은 #4257 max/ultra effort + clamp 진단 만료)에서도 같은 경로가 그대로입니다.DSH는
src/integrations/registry.ts에서sourcePreservingYaml: { path: ["llm-pi-ai", "providers", "opencodex"] }로 등록됩니다. 적용은src/integrations/writer.ts가patchYamlFragmentSource(src/integrations/omp-yaml-source.ts)를 호출하고, 결과가null이면uses YAML source opencodex cannot patch… left alone으로refuse합니다. 스냅샷도 안 남으니 복원 센터가 비는 것도 설계상 맞습니다(쓰기 전에 거절).보고자가 정리한 두 실패는 HEAD에서도 그대로 재현됩니다. (A)
providers: {}/ (B) 값 없는providers:→null. (C) 형제 프로바이더가 있는 평문 블록맵 / (E)llm-pi-ai키 자체 없음 → 패치 OK. 후속 댓글의 flow-styleproviders: { … }도null. 원인은 코드와 한 줄로 맞습니다.isPlainBlockKey는key:뒤에 인라인 값이 없는 평문 블록 키 줄만 인정합니다. 그래서providers: {}줄은 매치가 안 되고, 파서는 키를 이미 본 상태라locatePath가 「없는 키」분기로 가지 못하고null을 반환합니다. (B)는 줄은 매치되지만readPath가null이라isPlainRecord가드에서 탈락합니다. flow 케이스는 걷기는 지나가도 블록 삽입 결과가 재파싱 시semanticallyMatches에서 깨져 다시null입니다.우선순위 68인 이유: 깨끗한 DSH 프로필·DSH가 스스로 쓴 파일에서 연동 토글이 사실상 영원히 실패하고, 메시지가 「주석/포맷」으로 잘못 가리킵니다. 재현·원인·제안 수정이 모두 구체적이고, 범위는
omp-yaml-source.ts의 경로 찾기(그리고 거절 메시지 구분)에 가깝습니다. 다만 소스 보존 패처의 fail-closed 철학을 깨지 않게 빈 컨테이너/flow를 어디까지 받을지는 메인테이너 판단이 필요합니다. 지금 팁 레인(카탈로그/effort·허브)과 무관한 독립 버그이고, types.ts/config.ts 분할에도 안 걸립니다. 닫힌 #4197(DSH atomic replace ownership)과는 다른 축입니다. 중복으로 닫지 마세요.라인 78-83 -
src/integrations/omp-yaml-source.tsisPlainBlockKey- 정규식이key:[ ]*(?:#.*)?$라서providers: {}/providers: []같은 인라인 빈 컬렉션 줄을 키로 인정하지 않음라인 195-212 - 같은 파일
locatePath- 매치 0개인데readPath(parsed, prefix) !== undefined면 「없는 키」삽입이 아니라 즉시null(빈{}가 여기에 걸림). 중간 depth에서!isPlainRecord(readPath(...))면 역시null(값 없는providers:→ 파서 null)라인 323-338 -
patchYamlFragmentSource- upsert가 바이트를 만들어도semanticallyMatches실패 시null. flow-style 컨테이너에 블록 삽입하면 여기로 떨어짐(후속 댓글과 동일)라인 400-402 -
src/integrations/writer.ts- 패치null을 전부 같은 unsafe 「comments or formatting」문장으로 거절. 빈 컨테이너·미지원 스타일·진짜 주석 위험이 한 메시지로 섞임경로
src/integrations/registry.tsdsh.sourcePreservingYaml- 경로["llm-pi-ai","providers","opencodex"]확인. 이 이슈는 DSH 전용 증상이지만 패처는 OMP 등 같은 모듈을 쓰는 클라이언트에도 같은 빈/{}함정이 있음경로
tests/clients/omp-yaml-source-inline-comments.test.ts- 중첩 인라인 주석 거절만 있고, 빈providers: {}·flow map·「키만 있고 값 null」삽입 회귀가 없음경로/심볼 - types/config 분할 - 해당 없음. 닫지 말 것
메인테이너의 판단이 필요한 지점
{}/[])과 「키만 있는 null」을 missing-leaf 삽입으로 받을지, 아니면 문서/UI에서 「형제 프로바이더를 하나 두거나 키를 지우라」고만 안내할지providers를 패처가 받아 쓸지, 거절을 유지하되 메시지를 「flow mapping/sequence는 지원하지 않음」으로 갈라 쓸지omp-yaml-source.ts공통 동작으로 고칠지(OMP 등 공유)너의 추천
열어 두세요.
dev에 작은 버그픽스 PR을 받는 편이 맞습니다. 1순위는locatePath/isPlainBlockKey가 빈 컨테이너를 「없는 자식 삽입」으로 취급하게 해, 보고 재현 A/B와 케이스 E(키 없음)를 같은 성공 경로로 맞추는 것입니다. flow-style은 같은 PR에서 지원하기 어렵면 거절은 유지하되 writer 메시지를 스타일 미지원으로 분리하세요. 회귀는tests/clients/에providers: {}/ 값 없는providers:/ 형제 있는 블록맵 / flow map 네 줄이면 충분합니다. types/config 분할과 무관하니 리베이스 대신 이 이슈 기준으로 새 PR을 열면 됩니다.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Sep 11, 2026
Fixed on
devby #4268.An empty
providerscontainer — both the valueless block key and{}— is now adopted. A populated flow container is still refused, but the message names that as the cause instead of blaming comments.Closing manually: pull requests here target
dev, so GitHub'sCloseslinkage only fires on a merge intomain. This will reach users in the next release.