DSH plugin Office-to-PDF fails: Windows paths past MAX_PATH

TroubleshootingPublished 2026-10-03Author: DeepSeek Plugin Market
DeepSeek HarnessDSHWindowsdesktop packagingLibreOfficeKitMAX_PATHlong pathsOffice to PDF
On Windows, prepared-runtime Office conversion works but packaged-runtime fails with Unknown LibreOfficeKit error: packaging pushes the deepest path past 260.

When building the desktop package locally on Windows, Office conversion under the prepared runtime passes normally (DOCX, XLSX, PPTX to PDF passed), but the packaged runtime always fails on DOCX with OfficeToPdfError: LibreOffice conversion failed / [cause]: ConversionError: LibreOffice native conversion failed: Unknown LibreOfficeKit exception. That error leads you toward two wrong directions — "the native package is corrupted" or "the asar path was passed incorrectly" — while the reporter has already ruled both out: the two native packages have identical file count, size, and per-file SHA-256, and what is passed to the helper really is the physical path under app.asar.unpacked (#7485). There is exactly one real variable: the packaging layout pushes the path of LibreOffice's own deepest resource file past Windows' MAX_PATH (260). Map the same engine to C:\lo-test with a Junction and everything returns to normal. This article goes "triage first → converge variables with A–F → rule out three items → locate the deepest file → the Junction test → three fixes", and closes by explaining why this defect only shows up on developer machines.

Triage first: why the error message sends you the wrong way

In one sentence: at the DSH layer every unknown low-level conversion error is folded into the same 'failed', and Unknown LibreOfficeKit exception lands exactly in that branch, so the original information is lost during the mapping — which means reasoning backwards from the error text is futile; you must use an external measure (here, path length).

The mapping point is packages/document/office-to-pdf/src/index.ts:265-271: known codes are thrown as-is, unknown ones are uniformly folded into 'failed'. So:

  1. The top-level error you see is OfficeToPdfError, which only tells you "conversion failed";
  2. The [cause] ConversionError: … Unknown LibreOfficeKit exception is the fallback type given by the native helper and carries no specific failing object;
  3. The failure site @deepseek-ai/libreoffice-kit runNative() only says "at the native conversion step", not which resource path failed to open.

So the correct troubleshooting posture is not to stare at that text, but to treat it as "unknown cause" and then prune the candidate space with an external measure. This also incidentally suggests: if the code-mapping branch could carry the underlying exception's type or text, this class of report would be far easier to investigate (an improvement suggestion, not a conclusion of this article).

Locating it: A–F isolation, ruling out misjudgments, and the Junction test

Step one: converge the variable to "dsh root" with A–F isolation

The reporter's most valuable move was doing six cross combinations to squeeze the deciding variable down to one: whether dsh root is prepared or packaged.

Environment and command:

text
OS: Windows
tag: dsh-v0.1.7-alpha.1
desktop target: win32-x64
Electron: 44.0.0
electron-builder: 26.15.3
bundled Node: 24.18.1
bundled pnpm: 11.7.0
source directory: C:\PROJETS-PERSO\deepseek-harness

pnpm.cmd run package:desktop:win:x64:unsigned

The six combinations:

Combinationdsh rootElectronruntimeResult
ApreparedpreparedpreparedPASS
BpackagedpackagedpackagedFAIL
CpackagedpreparedpackagedFAIL
DpreparedpackagedpreparedPASS
EpackagedpreparedpreparedFAIL
FpreparedpreparedpackagedPASS

The pattern is very clean: whenever dsh root is packaged it fails; whenever it is prepared it passes. So two variables can be ruled out in one go:

  • Electron executable is not the deciding factor (A/C and D/B comparisons do not change the conclusion);
  • bundled runtime is not the deciding factor (A/F does not change the conclusion).

The significance of this step is that it turns "guess which part of packaging differs" into "there must be a physical difference at the dsh root layer".

Step two: rule out the three most common misjudgments

Before moving toward "path length", explicitly kill three more naive hypotheses, or the conclusion will keep being questioned.

1. Native package content difference — ruled out

Compare the engine in the two places, item by item:

text
Prepared: …\targets\win-x64\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64
Packaged: …\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64

file count identical
file size identical
per-file SHA-256 identical

So all three explanations — "electron-builder dropped files", "packaging corrupted the native package", "the package contents are damaged" — are out.

2. The app.asar virtual path being passed straight to the native helper — ruled out

The original suspicion was that require.resolve() returned a virtual path like …\resources\app.asar\dsh\node_modules\… under Electron. What actually printed was:

text
executable =
…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64\bin\libreoffice-kit.exe

programDirectory =
…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64\program\program

root =
…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64

It is already the real physical path corresponding to app.asar.unpacked, not the app.asar virtual path. And libreoffice-kit.exe itself starts fine — so it is not "the executable path is unreachable" either.

3. Electron / bundled runtime — already ruled out by A–F

See the comparison table in the previous section; no need to repeat it.

Step three: what actually exceeds the limit is "the deepest file in the tree", not the engine root

This is the most error-prone step in the whole investigation: measuring length from the engine root, neither layout exceeds 260, which would lead you to the wrong conclusion "this has nothing to do with MAX_PATH"; you must recursively count the absolute path of every file in the tree.

First look at the result of recursive counting per file (the same win-x64 build, source root D:\WorksPlace\deepseek-harness = 30 characters):

Directory treeFilesLongest pathFiles >260
prepared (targets\win-x64\dsh\node_modules\…)20502150
packaged (…\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\…)2050275385

The deepest one is:

text
…\program\share\config\soffice.cfg\modules\simpress\popupmenu\pagepanecanvasmaster.xml

So there is no need to bring in "the path of the document being converted" or "a temp/profile directory" to explain it — the packaging layout pushes LibreOffice's own resource files past the threshold. The discriminating experiment the reporter proposed (measure the input/output document paths) therefore already has its answer: what exceeds the limit is the resource path under the programDirectory that runNative() receives, not the document path.

The two measurements' conventions must be aligned

The original report gave prepared ~218 / packaged ~278 and said there are about 706 files >= 260. The second reproducer's source root was 3 characters shorter, giving 215 / 275 and 385 files >260. The correspondence is:

text
215 + 3 = 218
275 + 3 = 278
difference 60 characters = unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\

The two line up exactly, which says the same quantity was measured, only with different counting conventions for >=260 versus >260. This is also why a report must spell out the comparison operator and which layer is measured — they decide whether the conclusion is "irrelevant" or "it is exactly this".

Step four: the Junction test — the same engine, only the path changes

The decisive A/B approach: change not a single file of the packaged engine, only map it to a short path with a Windows Junction, keeping every other condition unchanged.

powershell
New-Item -ItemType Junction `
  -Path 'C:\lo-test' `
  -Target 'C:\PROJETS-PERSO\deepseek-harness\apps\desktop\.desktop-build\targets\win-x64\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64'

Then have the runtime use:

text
executable:       C:\lo-test\bin\libreoffice-kit.exe
programDirectory: C:\lo-test\program\program

Held constant:

text
same packaged engine
same files
same SHA-256
same runtime
same Electron
same smoke test
same input/output/profile logic

Result:

text
Long physical path -> FAIL
C:\lo-test         -> PASS
desktop runtime: DOCX, XLSX, PPTX to PDF passed

Why does the Junction work? Because MAX_PATH constrains the full path of every file, a short-path Junction shortens the common prefix of every file at once — and the deepest file benefits just the same. So "a Junction passes" and "this is a path-length problem" are not contradictory; they are two faces of the same thing.

Mechanism: the two conditions for long paths and one easy pitfall

Long paths need two conditions: turning on the registry key is not enough

In one sentence: breaking past MAX_PATH on Windows requires LongPathsEnabled=1 and a longPathAware declaration in the process manifest at the same time; when the latter is missing the registry key is decorative, so "have the user turn on long paths" is not a viable fix.

  • Searching the whole apps/desktop directory for longPathAware and LongPathsEnabled — no hits.
  • On the second reproducer's machine HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1, and the packaged smoke still failed.

Together these explain it: even if the runtime environment turns on the registry key, the packaged process still behaves according to MAX_PATH. LibreOffice and its dependent components may not all use long-path-capable Windows APIs, so this road should never have been relied on.

A pitfall you must avoid: a Junction on the repo root does not help

This one was stepped on by the reproducer and written into the conclusion; copying the wrong direction wastes a lot of time:

  • Ineffective usage: make a Junction of the repo root, then invoke the build through the short path — fails, and the path in the error is still the expanded long path. The reason is that Node's realpathSync() (and import.meta.url) expand the junction back to the real path.
  • Effective usage: make the Junction on the engine directory, and actually hand that short path string to the helper (C:\lo-test\bin\libreoffice-kit.exe).

One-sentence conclusion: what decides the outcome is "the path string actually handed to the native process", not whether a short-path alias exists in the filesystem.

Fixes: shallow directory / shorter source root / pre-packaging preflight

In one sentence: move LibreOfficeKit's physical location from resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit-win32-x64 to a shallow directory such as resources\lok, giving both the build and the runtime headroom at the same time.

Target structure:

text
resources\
  lok\
    bin\
    program\

instead of:

text
resources\
  app.asar.unpacked\
    dsh\
      node_modules\
        @deepseek-ai\
          libreoffice-kit-win32-x64\

Then have @deepseek-ai/libreoffice-kit or the Desktop runtime explicitly use this native engine's physical path.

Why the shallow directory is "effective and sufficient" — a three-part path budget

Measured: engine root 190 characters, deepest file 275, relative depth 85 characters. Plug in the three cases:

text
current installed build         resources(69) + 75 + 85        = 229   < 260   ✅  already safe
installed build with resources\lok   73 + 85            = 158   < 260   ✅
build-time with resources\lok       114 + 14 + 85      = 213   < 260   ✅

This table also explains a counterintuitive fact: the layout after the current installation is already safe (229); the failure only happens in the deeper output directory during the build (275). So this defect only appears on developer machines and end users never encounter it — the value of the shallow-directory change is giving both paths headroom, not just saving one product that is already in trouble.

Fix two (no code change, usable now): shorten the source root

In one sentence: copy the whole repo to a short path and re-run the full pipeline; measured, it passes — the cost is accepting a very short source root.

The reproducer put the repo at D:\dsh (6 characters) and re-ran the full packaging pipeline:

text
source root D:\dsh  (6 characters)
  → longest file path 251, files >260: 0
  → both smokes pass (prepare stage + packaged stage)
  → produces an installer, which installs, launches, and runs successfully

From this comes an actionable constraint:

text
win-x64 unsigned packaging requires a source-root path of ≲ 15 characters (longest suffix ≈ 245, 260 − 245)

This constraint has another source difference worth noting: the official release path uses artifacts/, while local unsigned testing uses unsigned-artifacts/ (6 characters longer). The two budgets differ and neither is documented anywhere — which means this problem may surface during local testing and be coincidentally masked in official releases.

Fix three (guardrail): a path-budget preflight before packaging

In one sentence: before the preparation stage of package-target.ts, compute the absolute path length of the deepest resource file once; if over the limit, fail before the time-consuming work starts and tell you directly "how short the source root needs to be".

What this guardrail addresses is investigation cost: right now the situation is twenty minutes of work followed by an error pointing at LibreOffice, when the real cause is path length. The preflight does exactly one thing — replace "a misleading error 20 minutes later" with "stating up front how far off you are". It is also a temporary insurance even when neither fix one nor fix two is done.

Two process observations: why nobody ever noticed this constraint

These two are not technical mechanisms, but they explain "why it is still here today":

  1. Desktop packaging is not wired into CI. Checking all 19 workflows under .github/workflows/, there is no desktop job — this path has no CI signal.
  2. The constraint is undocumented. Searching apps/desktop/, docs/, and .agents/notes/ for MAX_PATH / 260 / long path and similar keywords returns no hits.

So the fact that "local unsigned packaging has a hard constraint on source-root length" has neither an automated check nor any written record, and can only be discovered by stepping on it. Writing the constraint into the docs and wiring the check into CI matters as much as fixing the code.

Troubleshooting notes

  1. First check whether the error has been folded. OfficeToPdfError folds every unknown code into 'failed'; when you see Unknown LibreOfficeKit exception, stop reasoning backwards from the text and switch to an external measure.
  2. Change only one variable at a time during isolation tests. Only after A–F exhaustively covers the three variables dsh root / Electron / runtime do you earn the right to say "the deciding factor is X".
  3. When comparing two artifacts, compare contents first, then layout. If all SHA-256s match, immediately rule out "packaging corrupted files" and move your attention to physical paths.
  4. Measure length at the right layer. The engine root may be within 260 for both, while what exceeds it is usually the deepest resource file in the tree (such as program\share\config\...). In a report, spell out "measured from what" and "> or >=".
  5. Give "by how much" in the report. A count alone only lets people accept "there is a problem"; giving a number like 276 (+16) lets them judge whether it is "barely off" or "way off".
  6. Do not count on the Windows long-path switch. It needs LongPathsEnabled=1 plus a longPathAware process manifest; the latter is not found anywhere in the repo, and measured, turning on the registry key does not help.
  7. Use a Junction in the right place. On the repo root it gets expanded by realpathSync / import.meta.url and stops working; on the engine directory, with the runtime explicitly using the short path string, it works.
  8. Remember it is "the path string handed to the native process" that decides, not whether an alias exists. This determines how every workaround must be written.
  9. Distinguish build-time and runtime budgets. The current installed layout (229) is already safe; only the build-time output directory (275) exceeds the limit — a fix must cover both.
  10. Double-insure the short-root constraint with docs and CI. Local unsigned-artifacts is 6 characters longer than official artifacts, the budgets differ and are undocumented, which makes "explodes locally, passes officially" (or the reverse) very likely.

The most worthwhile takeaway from this report is not "LibreOffice has a long-path bug", but the difference between two measurement conventions: measured from the engine root, neither layout exceeds the limit and the conclusion would be "irrelevant to MAX_PATH"; measured from the deepest file in the tree, you see 385 files over the limit and the truth surfaces. If you also do desktop redistribution or local packaging on Windows, do two things now: switch the source root to a short path (something like D:\dsh, measured to pass) so this constraint does not waste half a day, and add a path-length preflight to your packaging script so the next occurrence reports at build start instead of disguising itself as a LibreOffice error 20 minutes later. Centralizing plugin install/uninstall, update confirmation, and system logs in DSH Plugin Hub makes environment problems along the local packaging chain easier to cross-reference.

DSH Plugin Hub · Install confirmation

Source: Discussion #7485.

FAQ

Why does the prepared runtime pass but the packaged runtime always fail — are they not the same LibreOffice?

They are the same, byte-for-byte identical — the reporter compared the two libreoffice-kit-win32-x64 copies and the file count, file sizes, and per-file SHA-256 all matched. What changes is the **physical absolute path**: the packaged layout adds another unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\ segment, 60 characters deeper than prepared. When LibreOfficeKit internally accesses its own program\share\... resources, the deepest file therefore crosses the 260-character traditional Windows path limit.

I already enabled long path support in Windows, so why does it still fail?

Because Windows long paths require **both** conditions at once: the registry HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1, **and** a longPathAware declaration in the process manifest. When the latter is missing, the registry setting has no effect for the process in question. The reporter measured this on a machine with LongPathsEnabled=1 and the packaged smoke still failed; searching the repository for longPathAware also returns nothing. So "ask the user to turn on a registry key" is not a viable fix direction.

I heard you can work around it by making a Junction from the repo root to a short path — why did that not work?

Because Node's realpathSync() and import.meta.url expand the Junction back to the real path, so the build still receives the long path. The usage that works is different: **put the Junction on the LibreOffice engine directory** and have the runtime explicitly use the short path string (C:\lo-test\bin\libreoffice-kit.exe). What matters is **the path string actually handed to the native process**, not whether a short-path alias exists in the filesystem.

Will officially released installers hit this defect?

Basically no. Working backwards from the measured numbers: the layout depth after installation is about 229, already within 260; the failure only happens in the deeper output directory during the **build** (about 275). This also explains why the report always came from developer machines and end users never reproduced it. The value of the fix is leaving headroom for **both** the build and runtime paths, not saving a product that is already broken.

Have the maintainers changed it? What can I do without changing code?

As of that discussion's progress, the shallow-directory approach is a suggested direction and no merged change has been seen. The no-code-change approach right now is to move the source root to a short path and re-run (measured: a 6-character root such as D:\dsh brings the longest file path down to 251, zeroes out the over-limit files, passes both smokes, and produces an installer that installs and launches successfully). Converted, win-x64 unsigned packaging requires a source root of roughly ≲ 15 characters.

Related Terms

MAX_PATH
The traditional 260-character Windows path limit (the fully qualified path). It constrains **the full path of every file**, not the path of some directory root — which means that *which layer you measure* can itself decide whether the conclusion is right or wrong: measured from the engine root it is usually within limits, but measured from the deepest file in the tree it is over the limit.— https://github.com/deepseek-ai/deepseek-harness/discussions/7485
longPathAware (process manifest declaration)
Windows' second precondition for letting a process break past MAX_PATH. Turning on `LongPathsEnabled=1` in the registry alone is not enough: the executable's application manifest must also declare that the process is long-path aware. A repository-wide search for `longPathAware` / `LongPathsEnabled` finds nothing, which is exactly why "turning on the registry key does not help".— https://github.com/deepseek-ai/deepseek-harness/discussions/7485
prepared / packaged (two desktop artifact layouts)
prepared is the runtime directory written directly during the build (`targets\win-x64\dsh\node_modules\...`); packaged is the unpacked directory produced by electron-builder (`...\unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\dsh\node_modules\...`). The two have identical contents, but the latter adds one more `unsigned-artifacts\win-unpacked\resources\app.asar.unpacked\` layer — a 60-character physical path-depth difference.— https://github.com/deepseek-ai/deepseek-harness/discussions/7485
path-budget preflight (suggested guardrail)
Before the preparation stage of `package-target.ts`, compute the absolute path length of the deepest resource file for the target layout, and if it is over the limit **fail before the time-consuming work starts**, reporting the shortest source-root length required. The goal is to replace "an error pointing at LibreOffice 20 minutes later" with "stating up front how far off you are".— https://github.com/deepseek-ai/deepseek-harness/discussions/7485

Sources