Replies: 7 comments 1 reply
|
You wondered whether forcing I reproduced this on a pristine checkout at - const resolutionMode = packaged ? 'runtime' : options.resolutionMode ?? 'runtime'
+ const resolutionMode = packaged ? 'runtime' : options.resolutionMode ?? 'link'Same command each time, from the repo root: node --import tsx/esm apps/cli/src/bin.ts --profile headless \
"Read package.json and reply with only the value of its name field. Do nothing else."That task needs exactly one
So One more data point on the boundary, in case it helps narrow the fix: on my install every session log back to 2026-09-13 had zero occurrences of this message, and after the Your note that the crash is unrecoverable by construction — no synthetic results where cancellation writes |
|
补一份独立复现与归属说明(Windows 11,Node v26.7.0,同一 commit 1)这是启动器层面的问题,与第三方插件无关。 契约两端都在 DSH 核心里:
第三方插件不参与这条路径:我复核的 profile 里没有任何社区插件把 2)两份实例是启动器自己造的,证据是进程内的模块清单。 在以
同一探针里: 3)修法(任选其一即可):
4)同源的二阶症状: 源码启动下,插件 |
|
已把这批 本帖属于其中的
本地 CLI 聚焦测试 16/16、tools scheduler key 测试和 CLI/tools/agent-loop TypeScript build 已通过。详细补丁、用户恢复步骤、会话处理和回归门禁见上面的集中回复。 |
|
Confirmed independently on macOS (Darwin 25.6.0, arm64) with the same clean checkout at Probe: the split needs only the importer path No profile boot, no model request. Symlink the package into a scratch // /tmp/probe/outside.mjs — importer path has no /node_modules/
const m = await import('@deepseek-ai/dsh-tools')
globalThis.__outside = m.TOOL_RUNTIME_SCHEDULER
// /tmp/probe/node_modules/inside.mjs — importer path contains /node_modules/
const m2 = await import('@deepseek-ai/dsh-tools')
globalThis.__inside = m2.TOOL_RUNTIME_SCHEDULERnode --import tsx/esm /tmp/probe/run.mjs
# same module instance? falseTwo files, identical specifier, two module instances — the importer living under Fix check - const resolutionMode = packaged ? 'runtime' : options.resolutionMode ?? 'runtime'
+ const resolutionMode = packaged ? 'runtime' : options.resolutionMode ?? 'link'plus the assertion that owns this default at After restarting the process (a new session is not enough — the launcher default is read at boot):
Sessions that already wrote a On which default is right
|
|
补充一个在 macOS 上独立复现、验证过的最小解析层修复(非官方本地临时方案),供维护者评估。 环境: 本地进一步定位到 ESM fallback 的 parent: 本地方案是在 生产修复补丁diff --git a/packages/boot/app-boot/src/profile-resolution/resolver.ts b/packages/boot/app-boot/src/profile-resolution/resolver.ts
index 959482c02a..ada1c81611 100644
--- a/packages/boot/app-boot/src/profile-resolution/resolver.ts
+++ b/packages/boot/app-boot/src/profile-resolution/resolver.ts
@@ -690,7 +690,11 @@ export function installProfileResolution(
if (cacheable && !(result instanceof Promise)) state.esm = result
return result
}
- const routedParent = pathToFileURL(route.kind === 'fallback' ? route.entry.declarer : route.parent).href
+ // Source loaders exclude node_modules from path aliases; a workspace
+ // symlink must use the declaring package's physical location.
+ const routedParent = pathToFileURL(route.kind === 'fallback'
+ ? canonicalPath(route.entry.declarer)
+ : route.parent).href
if (behavior === 'enforce') {
const previous = delegatedEsm
delegatedEsm = { parent: routedParent, request }同时补充了不需要 API key 的源码启动回归:通过真正的 将下方测试补丁保存为 git apply dsh-source-tool-regression.patch
pnpm exec vitest run apps/cli/tests/source-launch.compat.spec.ts -t 'source profile imports'两个模式在没有生产修复时均复现完全相同的 本地验证还包括 profile resolver、resolution service、worker bootstrap 相关测试,以及构建后的 headless 和 SDK profile 冒烟。构建、lint、中英文文档检查和 41 项 doc-sync 门禁通过。 此补丁只解决本次源码加载触发条件,未验证第三方插件的重复安装、HMR 或已损坏会话的修复;没有改动会话格式。补丁目前保留在本地,未提交 commit、推送分支或创建 PR。 源码启动回归测试与模型 fixturediff --git a/apps/cli/tests/source-launch.compat.spec.ts b/apps/cli/tests/source-launch.compat.spec.ts
index 975e4c3591..e6d4ec00f7 100644
--- a/apps/cli/tests/source-launch.compat.spec.ts
+++ b/apps/cli/tests/source-launch.compat.spec.ts
@@ -1,16 +1,16 @@
-import { readFile } from 'node:fs/promises'
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
+import { LOADER_SMOKE_TEST_TIMEOUT_MS, resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
/**
- * Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts`
- * with the exact production runtime vector (`node --import tsx/esm`, the
- * vector the root `dsh` script invokes directly) and assert the
- * required-config diagnostic. The Node compatibility matrix runs this
- * WHOLE file, so a Node release changing module hooks or TypeScript handling
- * breaks this gate instead of every developer's `pnpm dsh`; the built-bin
- * suite covers the published `lib/` entry, not this source chain.
+ * Source-only compatibility checks for the `pnpm dsh` ESM hook.
+ * Configured plugins and workspace imports must share module identities
+ * through both native and PTC tool dispatch. The Node compatibility matrix
+ * runs this file independently of the published-bin tests.
*/
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
@@ -39,4 +39,62 @@ describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
expect(result.stderr).toContain('--profile <name> is required')
expect(result.stdout).toBe('')
}, 30_000)
+
+ it.each(['native', 'ptc'])('executes a %s tool through source profile imports', async (mode) => {
+ const cwd = await mkdtemp(join(tmpdir(), 'dsh-source-tools-'))
+ try {
+ const patch = join(cwd, 'tools.patch.yml')
+ const provider = new URL('./profiles/headless/tests/fixtures/source-tools-llm.mjs', import.meta.url).href
+ await writeFile(patch, [
+ '- id: llm-deepseek',
+ ' disabled: true',
+ '- id: session-title-llm',
+ ' disabled: true',
+ '- id: plugin-package-inventory-deepseek',
+ ' disabled: true',
+ '- id: sandbox-policy',
+ ' config:',
+ ' mode: danger-full-access',
+ '- id: approval',
+ ' config:',
+ ' policy: never',
+ '- id: tools',
+ ' config:',
+ ` mode: ${mode}`,
+ '- insert:',
+ ' - id: source-tools-llm',
+ ` name: ${JSON.stringify(provider)}`,
+ '',
+ ].join('\n'))
+ const launch = resolveExampleLaunch({
+ srcBin: join(repoRoot, dshSourceBin),
+ mode: 'src',
+ sourceImport: 'tsx/esm',
+ tsconfigPath: join(repoRoot, 'tsconfig.json'),
+ configArgs: ['--profile', 'headless', '--patch', patch, 'Write the source tool marker.'],
+ env: {
+ DSH_HOME: join(cwd, '.dsh'),
+ DSH_AGENTS_HOME: join(cwd, '.agents'),
+ DSH_TELEMETRY_DISABLED: '1',
+ DEEPSEEK_API_KEY: '',
+ },
+ })
+ const result = await execa(launch.command, launch.args, {
+ cwd,
+ env: launch.env,
+ input: '',
+ timeout: 30_000,
+ killSignal: 'SIGKILL',
+ reject: false,
+ })
+ const diagnostic = `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`
+ expect(result.timedOut, diagnostic).toBe(false)
+ expect(result.signal, diagnostic).toBeUndefined()
+ expect(result.exitCode, diagnostic).toBe(0)
+ expect(await readFile(join(cwd, 'source-tool.txt'), 'utf8')).toBe('SOURCE_TOOL_OK\n')
+ expect(result.stdout).toContain('SOURCE_TOOL_OK')
+ } finally {
+ await rm(cwd, { recursive: true, force: true })
+ }
+ }, LOADER_SMOKE_TEST_TIMEOUT_MS)
})
diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/source-tools-llm.mjs b/apps/cli/tests/profiles/headless/tests/fixtures/source-tools-llm.mjs
new file mode 100644
index 0000000000..33ab6dea18
--- /dev/null
+++ b/apps/cli/tests/profiles/headless/tests/fixtures/source-tools-llm.mjs
@@ -0,0 +1,45 @@
+/** Deterministic tool requests for the source-launch profile smoke. */
+
+import { LlmAdapter, ToolCallId } from '@deepseek-ai/dsh-llm'
+
+class SourceToolsAdapter extends LlmAdapter {
+ async * stream(options) {
+ const result = options.messages.flatMap(message => message.content)
+ .find(block => block.type === 'tool-result')
+ if (result === undefined) {
+ const ptc = options.tools.some(tool => tool.name === 'run_code')
+ const args = { file_path: 'source-tool.txt', content: 'SOURCE_TOOL_OK\n' }
+ const block = {
+ type: 'tool-call',
+ id: ToolCallId('source-tool-write'),
+ name: ptc ? 'run_code' : 'write',
+ arguments: JSON.stringify(ptc
+ ? { code: `return await tools.write(${JSON.stringify(args)})`, description: 'Write the source tool marker' }
+ : args),
+ }
+ yield { type: 'block-start', index: 0, blockType: 'tool-call' }
+ yield { type: 'block-end', index: 0, block }
+ yield { type: 'finish', reason: { kind: 'tool-calls' } }
+ return
+ }
+ if (result.isError) throw new Error(JSON.stringify(result.content))
+ const text = 'SOURCE_TOOL_OK'
+ yield { type: 'block-start', index: 0, blockType: 'text' }
+ yield { type: 'text-delta', index: 0, text }
+ yield { type: 'block-end', index: 0, block: { type: 'text', text } }
+ yield { type: 'finish', reason: { kind: 'stop' } }
+ }
+}
+
+/** Cordis plugin name. */
+export const name = 'source-tools-llm'
+/** Required model registry. */
+export const inject = ['llm']
+
+/**
+ * Register a keyless model that requests a real file write.
+ * @param {import('@deepseek-ai/cordis').Context} ctx - plugin context.
+ */
+export function apply(ctx) {
+ ctx.llm.registerAdapter(['deepseek-official'], new SourceToolsAdapter())
+} |
|
node apps/cli/lib/bin.js web |
|
We reproduced this independently and traced it to the profile fallback anchor: under pnpm the declaring manifest lives in a symlinked A minimal fix (normalize that anchor to its real path, leaving exports and loader hooks to Node/tsx), regression tests, and A/B verification (with and without build artifacts) plus an end-to-end run are in #7048, together with the fork branch and patch. |
Uh oh!
There was an error while loading. Please reload this page.
Environment
ddefc45fbc, master, clean working tree, aftergit pullfrom0d1f50007fpnpm dsh webfrom the source checkout (node --import tsx/esm apps/cli/src/bin.ts), Node v26.7.0, Linux x86_64web(bundles@deepseek-ai/dsh-base,@deepseek-ai/dsh-web-app), default DeepSeek Messages protocol@deepseek-ai/dsh-toolsindependenciesorpeerDependencies, and there is no second physical copy of the package: every@deepseek-ai/dsh-toolsposition in the checkout resolves through symlinks topackages/core/tools, the only@deepseek-aidirectory under the DSH home is the flat fallback whose entries are also symlinks into the checkout, and the profile's.pnpmdirectory holds no packagesSummary
After this update, the first tool call of the first turn after launch dies with
at
ctx.tools[TOOL_RUNTIME_SCHEDULER].prepare(call.exec)indsh-agent-loop. The turn ends withcode: UNKNOWNafter the assistant message and itstool/callevents are already durably recorded, and because notool/resultfollows, every later turn in that session fails withDeepSeek Messages tool calls need immediate results(locally thrown, no request is ever sent). The session is permanently unresumable, exactly as reported in #1337.Unlike #1337, #2078 and #2130, this needs no community plugin and no duplicate package on disk. The two module instances are created by the launcher itself, from the same package directory.
Reproduction
ddefc45fbc,pnpm install,pnpm run build.pnpm dsh web.linkmode also fixes it.Current behavior
turn/enderrorCannot read properties of undefined (reading 'prepare')(code: UNKNOWN) on the first tool dispatch.tool-callblocks is committed, onetool/callevent is written for the first call, and no results are written for either.DeepSeek Messages tool calls need immediate results,code: INVALID_REQUEST, thrown during serialization before any HTTP request.Why the update changed this
TOOL_RUNTIME_SCHEDULERis a module-localSymbol(packages/core/tools/src/index.ts:463) and the scheduler is a per-instance class field, soundefinedat that lookup means thetoolsservice instance and the importingdsh-agent-loopcame from two different copies of@deepseek-ai/dsh-tools.Commit
9ddef327a4("feat: resolution mode link to runtime") changed the launcher default inapps/cli/src/profile-boot.ts:263:Measured on the same checkout, with no plugins loaded:
@deepseek-ai/dsh-toolsresolves toimport.meta.resolve, from both.tsand.mjs)packages/core/tools/src/index.tsruntimegeneration's entries for@deepseek-ai/dsh-tools,dsh-agent-loop,@deepseek-ai/cordispackages/core/tools/lib/index.js,packages/core/agent-loop/lib/index.js,vendor/cordis/lib/index.jsSo the source plane (tsx + tsconfig
paths) and the artifact plane (the installed generation) are both live in one process.linkmode left resolution alone, which is why this worked before the update.Both halves of that table are reproducible in seconds on a clean checkout, with no model request:
I confirmed the packages themselves are single copies on disk; the duplicate is the module instance, not a directory. That is why none of the plugin-side remedies in #2130 apply here.
Expected behavior
A source launch should keep one module plane — or the scheduler lookup should survive the split — and a turn that dies mid-dispatch should leave a resumable session.
Two secondary notes
parseDshArgshas no resolution-mode flag,runClicallsrunProfilewithoutresolutionMode, and nothing reads an environment variable for it. A source-checkout user cannot selectlinkwithout editing source. It also meanspnpm run buildis now mandatory after every pull, because the packaged rows readlib/.agent-loopdocuments "Scheduler failure drains dispatches without committing synthetic recovery results", so a dispatch failure writes no results where cancellation writesABORTED_BEFORE_DISPATCHpairs. Any dispatch failure therefore poisons the session permanently. A synthetic result pair (or any recovery path) for failed dispatch would turn this class of bug into a single failed turn.Local unblocker (not a proposal)
Registering the symbol globally makes the lookup survive the split:
I run that plus
pnpm run build, applied and reverted automatically aroundgit pull, purely as a local unblocker. I have read #2130's trade-off argument and agree it is a stopgap rather than the fix; I am reporting the new trigger, not proposing this as the direction. Which direction is yours to choose — keeping source launches on a single plane, or making the collision fail loud and name both copies. DSH recommended I post this and drafted the preceding text. I hope this saves someone some time!All reactions