Map Host branded create failures and retry without broken presets.

Classify gateway/internal, workspace/*, and agent-preset/* into actionable IM codes, enrich field diagnostics, realpath-match workspaces, and retry session.create once without a failing Agent Preset.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-16 21:41:05 +08:00
parent f1c9abfd1d
commit f1d85d2985
8 changed files with 415 additions and 220 deletions

View file

@ -8,6 +8,12 @@ This file records the notable changes in each dsh-im release. Its format follows
### Fixed / 修复
- Host 带斜杠的错误码(如 `gateway/internal`、`workspace/invalid-path`、`agent-preset/not-found`)现在会正确映射为 `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE` / `PRESET_UNAVAILABLE`;`last-message-failure.json` 额外写入 workspace、Host details 与脱敏 message,便于现场对上真实 RPC。
Host slash-branded failure codes (e.g. `gateway/internal`, `workspace/invalid-path`, `agent-preset/not-found`) now map to `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE` / `PRESET_UNAVAILABLE`; `last-message-failure.json` also records workspace, Host details, and redacted messages for field triage.
- `session.create` 在 Agent Preset 失败或 Host `internal` 时会自动去掉该 preset 重试一次,避免坏的成员/机器人预设覆盖把首次建会话永久卡死;工作区路径匹配进一步用 `realpath`(存在时)对齐 Host 规范路径。
`session.create` now retries once without the Agent Preset after preset failures or Host `internal`, so a bad member/bot preset override cannot permanently block first Session creation; workspace path matching also prefers `realpath` when the directory exists, matching Host canonicalization.
- Windows 下机器人工作区路径与 Host `workspace.list` 仅盘符/路径大小写不同(如 `D:\…` vs `d:\…`)时,不再因严格字符串匹配漏掉已有工作区而去 `workspace.create` / `session.create`,避免误报 `SESSION_CREATE`。
On Windows, when the bot workspace path differs from a Host `workspace.list` entry only by drive/path casing (e.g. `D:\…` vs `d:\…`), IM now reuses that workspace instead of missing it via exact string match and falling through to `workspace.create` / `session.create` (which surfaced as `SESSION_CREATE`).

View file

@ -571,7 +571,7 @@ var React26 = __toESM(require("react"), 1);
// package.json
var package_default = {
name: "dsh-im-ops",
version: "4.9.1-ops.34",
version: "4.9.1-ops.35",
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
keywords: [
"deepseek-harness",

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"name": "dsh-im-ops",
"version": "4.9.1-ops.34",
"version": "4.9.1-ops.35",
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
"keywords": [
"deepseek-harness",

View file

@ -1,5 +1,6 @@
import { spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { existsSync, realpathSync } from 'node:fs';
import { isAbsolute, resolve } from 'node:path';
import { adoptRegisteredWorkspaceSession } from './harness-session-binding.mjs';
@ -214,21 +215,46 @@ function workspacePaths(value) {
}
/**
* Windows treats drive/path casing as equivalent; Host may store `d:\…`
* while the bot UI persists `D:\…`. Match like Win32, not POSIX string equality.
* Canonicalize for Host workspace identity: prefer realpath when the directory
* exists (matches Host `realpathNormalize`), else resolved spelling. Win32 also
* folds drive/path casing so `D:\…` and `d:\…` collide the same way Host does.
*/
function canonicalWorkspacePath(value) {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
const resolved = resolve(trimmed);
try {
if (existsSync(resolved)) return realpathSync(resolved);
} catch {
// Fall through to the resolved spelling when realpath is unavailable.
}
return resolved;
}
function sameWorkspacePath(left, right) {
if (typeof left !== 'string' || typeof right !== 'string') return false;
const wanted = left.trim();
const got = right.trim();
if (!wanted || !got) return false;
const a = resolve(wanted);
const b = resolve(got);
const a = canonicalWorkspacePath(left);
const b = canonicalWorkspacePath(right);
if (!a || !b) return false;
return process.platform === 'win32'
? a.toLowerCase() === b.toLowerCase()
: a === b;
}
function isPresetCreateFailure(error) {
const code = typeof error?.code === 'string' ? error.code : '';
return code.startsWith('agent-preset/') || code.startsWith('agent-preset-');
}
function isInternalCreateFailure(error) {
const code = typeof error?.code === 'string' ? error.code : '';
return code === 'internal'
|| code === 'gateway/internal'
|| code.endsWith('/internal')
|| code === 'harness-http-failed'
|| code === 'harness-rpc-rejected';
}
function findListedWorkspace(items, workspacePath) {
if (!Array.isArray(items)) return null;
return items.find((item) => sameWorkspacePath(item?.path, workspacePath)) ?? null;
@ -939,14 +965,60 @@ export class HarnessClient {
}
async createSession(options = {}) {
const { agentPreset: requestedPreset, ...rpcOptions } = options;
const { agentPreset: requestedPreset, workspace, ...rpcOptions } = options;
await this.ensureRunning(rpcOptions);
const workspaceId = await this.workspaceId(rpcOptions);
const workspacePath = typeof workspace === 'string' && workspace.trim()
? workspace.trim()
: this.#workspace;
const workspaceId = await this.workspaceId({
...rpcOptions,
...(workspacePath ? { workspace: workspacePath } : {}),
});
const payload = { workspaceId };
const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
if (agentPreset != null) payload.agentPreset = agentPreset;
const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
return created.sessionId;
const annotate = (error) => {
if (error && typeof error === 'object') {
if (workspacePath) error.workspace = workspacePath;
if (error.details == null || typeof error.details !== 'object') {
error.details = { workspaceId };
} else if (!Object.hasOwn(error.details, 'workspaceId')) {
error.details = { ...error.details, workspaceId };
}
if (payload.agentPreset && !Object.hasOwn(error.details, 'agentPreset')) {
error.details = { ...error.details, agentPreset: payload.agentPreset };
}
}
return error;
};
try {
const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
return created.sessionId;
} catch (error) {
// Broken per-chat/bot presets often surface as Host internal on create.
// Retry once without the preset so a bad override does not block the chat.
if (
payload.agentPreset
&& (isPresetCreateFailure(error) || isInternalCreateFailure(error))
) {
console.warn(
`[${this.#logPrefix}] session.create failed with agentPreset `
+ `${payload.agentPreset} (${error?.code ?? 'unknown'}); retrying without preset`,
);
try {
const created = await this.rpc(
'session.create',
{ workspaceId },
30_000,
rpcOptions,
);
return created.sessionId;
} catch (retryError) {
throw annotate(retryError);
}
}
throw annotate(error);
}
}
async renameSession(sessionId, title, options = {}) {

View file

@ -165,6 +165,23 @@ function rpcMethod(error) {
return typeof error?.method === 'string' ? error.method.trim() : '';
}
/** Host branded codes use `family/name`; older builds may emit bare `internal`. */
function isHostInternalCode(code) {
return code === 'internal'
|| code === 'gateway/internal'
|| code.endsWith('/internal');
}
function isHostWorkspaceCode(code) {
return code.startsWith('workspace/')
|| code.startsWith('workspace-');
}
function isHostPresetCode(code) {
return code.startsWith('agent-preset/')
|| code.startsWith('agent-preset-');
}
function failureCode(error) {
const code = typeof error?.code === 'string' ? error.code : '';
const method = rpcMethod(error);
@ -194,8 +211,16 @@ function failureCode(error) {
return 'HARNESS_PROTOCOL';
}
if (code === 'harness-turn-failed') return 'INTERNAL_UNKNOWN';
// Host RPC catch-all — refine by method so field ops see workspace vs create vs generic.
if (code === 'internal' || ['harness-http-failed', 'harness-rpc-rejected'].includes(code)) {
// Branded Host failures (slash codes) before the generic internal catch-all.
if (isHostPresetCode(code)) return 'PRESET_UNAVAILABLE';
if (isHostWorkspaceCode(code)) return 'WORKSPACE_UNAVAILABLE';
if (code === 'session/workspace-attach-failed' || code === 'session/conflict') {
return 'SESSION_CREATE';
} // Host RPC catch-all — refine by method so field ops see workspace vs create vs generic.
if (
isHostInternalCode(code)
|| ['harness-http-failed', 'harness-rpc-rejected'].includes(code)
) {
if (method === 'workspace.create' || method === 'workspace.list') {
return 'WORKSPACE_UNAVAILABLE';
}
@ -212,8 +237,6 @@ function failureCode(error) {
if (MISSING_SESSION_CODES.has(code)) return 'SESSION_NOT_FOUND';
if (code === 'agent-busy') return 'SESSION_BUSY';
if (code === 'workspace-session-stale') return 'SESSION_STALE';
if (code.startsWith('workspace-')) return 'WORKSPACE_UNAVAILABLE';
if (code.startsWith('agent-preset-')) return 'PRESET_UNAVAILABLE';
if (code.startsWith('image-') || code.startsWith('inbound-file-')
|| code === 'attachment-error') return 'INPUT_INVALID';
@ -262,10 +285,39 @@ const PERSISTED_FAILURE_CODES = new Set([
'MODEL_TRANSPORT',
]);
function redactedDetail(value, { max = 160 } = {}) {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? trimmed.slice(0, max) : null;
}
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'boolean') return value;
return null;
}
function diagnosticDetails(error) {
const details = error?.details;
if (!details || typeof details !== 'object' || Array.isArray(details)) return null;
const out = {};
for (const key of [
'path', 'workspaceId', 'sessionId', 'agentPreset', 'requestedPreset',
'existingPreset', 'requestedCwd', 'existingCwd', 'reason', 'name',
]) {
if (!Object.hasOwn(details, key)) continue;
const value = redactedDetail(details[key]);
if (value != null) out[key] = value;
}
return Object.keys(out).length > 0 ? out : null;
}
async function persistMessageFailureDiagnostic(failure, error) {
try {
const dir = join(homedir(), '.dsh', 'integrations', 'dsh-im-ops');
await mkdir(dir, { recursive: true });
const details = diagnosticDetails(error);
const workspaceHint = typeof error?.workspace === 'string'
? redactedDetail(error.workspace, { max: 240 })
: null;
const payload = {
at: new Date(failure.at).toISOString(),
referenceId: failure.referenceId,
@ -275,6 +327,8 @@ async function persistMessageFailureDiagnostic(failure, error) {
errorCode: typeof error?.code === 'string' ? error.code.slice(0, 80) : null,
providerCode: typeof error?.providerCode === 'string' ? error.providerCode.slice(0, 80) : null,
method: typeof error?.method === 'string' ? error.method.slice(0, 80) : null,
workspace: workspaceHint,
...(details ? { details } : {}),
messages: diagnosticMessages(error).map((text) => text.slice(0, 240)),
};
await writeFile(

View file

@ -648,6 +648,47 @@ test('HarnessClient reuses Host workspaces when only Windows path casing differs
);
});
test('HarnessClient retries session.create without agentPreset after preset/internal failure', async () => {
const client = new HarnessClient({
baseUrl: 'http://127.0.0.1:3080',
workspace: '/tmp/dsh-feishu-workspace',
agentPreset: 'broken-preset',
autostart: false,
dshBin: 'dsh',
});
const creates = [];
client.ensureRunning = async () => undefined;
client.rpc = async (method, payload) => {
if (method === 'workspace.list') {
return {
items: [{
workspaceId: 'workspace-one',
path: '/tmp/dsh-feishu-workspace',
sessionIds: [],
}],
archivedSessionIds: [],
};
}
if (method === 'session.create') {
creates.push(payload);
if (payload.agentPreset) {
const error = new Error('agent-presets: preset "broken-preset" not found');
error.code = 'agent-preset/not-found';
error.method = 'session.create';
throw error;
}
return { sessionId: 'session-recovered' };
}
throw new Error(`unexpected method ${method}`);
};
assert.equal(await client.createSession(), 'session-recovered');
assert.deepEqual(creates, [
{ workspaceId: 'workspace-one', agentPreset: 'broken-preset' },
{ workspaceId: 'workspace-one' },
]);
});
test('HarnessClient asks do not control file-return tool availability', async () => {
const client = new HarnessClient({
baseUrl: 'http://127.0.0.1:3080',

View file

@ -117,6 +117,28 @@ test('session.create failures tell operators to check the bot workspace path', (
assert.match(failure.message, /工作区/);
});
test('Host branded gateway/internal on session.create maps to SESSION_CREATE', () => {
assert.equal(classifyMessageFailure({
code: 'gateway/internal', method: 'session.create',
message: 'failed to create session "session-x": Error: boom',
}, options).code, 'SESSION_CREATE');
});
test('Host branded workspace/invalid-path maps to WORKSPACE_UNAVAILABLE', () => {
assert.equal(classifyMessageFailure({
code: 'workspace/invalid-path', method: 'workspace.create',
message: 'cannot create a Workspace at "D:\\\\missing": ENOENT',
details: { path: 'D:\\missing' },
}, options).code, 'WORKSPACE_UNAVAILABLE');
});
test('Host branded agent-preset/not-found maps to PRESET_UNAVAILABLE', () => {
assert.equal(classifyMessageFailure({
code: 'agent-preset/not-found', method: 'session.create',
details: { agentPreset: 'gone' },
}, options).code, 'PRESET_UNAVAILABLE');
});
test('message failure text contains a safe code and traceable reference', () => {
const failure = classifyMessageFailure(new Error('secret-shaped internal detail'), options);
assert.deepEqual(failure, {