Skip to content

[Bug]: DSH integration refuses to apply when llm-pi-ai.providers is empty ("cannot patch without risking unrelated comments or formatting") #4260

Closed

Description

@a1980881039

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: system
  1. ocx start
  2. Open the dashboard, select DSH on the integration page, and toggle it on.
  3. Observe the refusal box. Nothing is written, and 恢复中心 registers no entry.
  4. ocx integration client status --client dsh reports state: absent and snapshotCount: 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: false

Dashboard 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 at parentIndent + 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

  1. a1980881039 commented on Sep 11, 2026

    @a1980881039
    Author

    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.providers is no longer empty, but it is rendered in flow style instead of a plain block map:

    llm-pi-ai:
      providers:
        {
          <provider>:
            {
              displayName: ...,
              apiKeyEnv: ...,
              api: openai-completions,
              baseURL: ...,
              models:
                [
                  {
                      id: ...,
                      name: ...,
                      contextWindow: 1000000,
                      reasoningEfforts: { off: null, low: low, medium: medium, high: high }
                    }
                ]
            }
        }
    ui-theme:
      preference: system

    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:

    locatePath: missing
    patchYamlFragmentSource: null
    upsertSource: produced text
    semanticallyMatches: false
    

    Here the walk does get past llm-pi-ai and providers, because providers: is a plain block key with no inline value, and containerEnd scans to the end of the block rather than stopping at the }. So locatePath returns a missing-leaf entry instead of null, and upsertSource renders an insertion. The rejection then happens at the final re-parse in patchYamlFragmentSource: the block-style insertion lands against a flow mapping, so the reparsed document no longer matches the expected document, semanticallyMatches returns 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:

    1. Accept flow-style containers in locatePath/upsertSource, or
    2. Keep refusing, but say so explicitly in the message, e.g. distinguish "this path is written in a style the patcher does not support (flow mapping/sequence)" from "the surrounding file has comments or formatting we would have to re-render".

    The diagnostic names are taken from src/integrations/omp-yaml-source.ts at the v2.50.0 tag; the reproduction harness called patchYamlFragmentSource with ["llm-pi-ai", "providers", "opencodex"] and a generated provider block, identical to the empty-container reproduction above.

  2. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 68 / 80

    설명

    이 이슈는 Integrations → DSH를 대시보드에서 켜도 설정 파일이 한 글자도 안 바뀌고, 중국어/영문 거절 상자만 뜨는 버그입니다. 메시지 뜻은 「이 YAML을 안전하게 고칠 수 없어서 그냥 두었다」인데, 실제로는 주석이나 이상한 포맷이 아니라 llm-pi-ai.providers가 비어 있거나(flow/{}) DSH가 다시 쓴 flow-style 맵일 때 소스 보존 패처가 스스로 손을 떼는 경우입니다. 보고 버전은 2.50.0이고, CURRENT dev HEAD 18e553a52(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-style providers: { … }도 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.ts isPlainBlockKey - 정규식이 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.ts dsh.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에서 「형제 프로바이더를 하나 두거나 키를 지우라」고만 안내할지
    • DSH가 내보내는 flow-style providers를 패처가 받아 쓸지, 거절을 유지하되 메시지를 「flow mapping/sequence는 지원하지 않음」으로 갈라 쓸지
    • 픽스를 DSH 경로만 특수 처리할 지, omp-yaml-source.ts 공통 동작으로 고칠지(OMP 등 공유)
    • 회귀 테스트에 A/B/flow 세 케이스를 최소로 넣을지, writer 거절 문구 구분까지 같은 PR에 묶을지

    너의 추천
    열어 두세요. dev에 작은 버그픽스 PR을 받는 편이 맞습니다. 1순위는 locatePath/isPlainBlockKey가 빈 컨테이너를 「없는 자식 삽입」으로 취급하게 해, 보고 재현 A/B와 케이스 E(키 없음)를 같은 성공 경로로 맞추는 것입니다. flow-style은 같은 PR에서 지원하기 어렵면 거절은 유지하되 writer 메시지를 스타일 미지원으로 분리하세요. 회귀는 tests/clients/에 providers: {} / 값 없는 providers: / 형제 있는 블록맵 / flow map 네 줄이면 충분합니다. types/config 분할과 무관하니 리베이스 대신 이 이슈 기준으로 새 PR을 열면 됩니다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    Fixed on dev by #4268.
    An empty providers container — 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's Closes linkage only fires on a merge into main. This will reach users in the next release.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions