mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
Classify session/workspace create failures and persist field diagnostics.
Map Host internal errors on session.create and workspace.create to actionable codes, and write last-message-failure.json so field hosts can triage without guessing from HARNESS_SERVICE. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f989b8bd63
commit
208c25ebbd
7 changed files with 276 additions and 219 deletions
|
|
@ -8,6 +8,9 @@ This file records the notable changes in each dsh-im release. Its format follows
|
|||
|
||||
### Fixed / 修复
|
||||
|
||||
- `session.create` / `workspace.create` 的 Host `internal` 错误不再笼统显示 `HARNESS_SERVICE`:分别映射为 `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE`,并写入 `%USERPROFILE%\\.dsh\\integrations\\dsh-im-ops\\last-message-failure.json`(含 method 与脱敏诊断),便于现场定位工作区路径问题。
|
||||
Host `internal` failures on `session.create` / `workspace.create` no longer collapse to generic `HARNESS_SERVICE`: they map to `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE`, and write `%USERPROFILE%\\.dsh\\integrations\\dsh-im-ops\\last-message-failure.json` (method + redacted diagnostics) for field triage of workspace path issues.
|
||||
|
||||
- 发消息时若聊天绑定的 Session 不在机器人当前配置工作区,或 history 探针返回非「会话不存在」错误,自动解绑并按机器人工作区 / Agent Preset / 默认模型新建 Session,避免换工作区后幽灵绑定一直报 `HARNESS_SERVICE`(含重建后的 Host/Client 产物 `lib/`)。
|
||||
When a chat’s bound Session is outside the bot’s configured workspace, or the history probe fails with a non-missing Host error, IM now clears the binding and creates a new Session using the bot workspace / Agent Preset / default model, instead of sticking on a zombie binding that surfaces as `HARNESS_SERVICE` (includes rebuilt Host/Client `lib/` artifacts).
|
||||
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ var React26 = __toESM(require("react"), 1);
|
|||
// package.json
|
||||
var package_default = {
|
||||
name: "dsh-im-ops",
|
||||
version: "4.9.1-ops.31",
|
||||
version: "4.9.1-ops.32",
|
||||
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",
|
||||
|
|
|
|||
419
lib/index.js
419
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dsh-im-ops",
|
||||
"version": "4.9.1-ops.32",
|
||||
"version": "4.9.1-ops.33",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ export default {
|
|||
'The bot and DeepSeek Harness use incompatible APIs. Ask an administrator to check the Harness address and update the related components.',
|
||||
'DeepSeek Harness 暂时无法完成请求,请稍后重试。':
|
||||
'DeepSeek Harness could not complete the request. Please try again later.',
|
||||
'无法在当前工作区创建新会话。请检查机器人工作区是否为绝对路径、目录存在、且运行 Host 的账号可访问;改回可用工作区后重试。':
|
||||
'Could not create a new Session in the current workspace. Check that the bot workspace is an absolute path, the directory exists, and the Host process can access it; then switch back to a working workspace and retry.',
|
||||
'等待模型回复超时,任务可能仍在运行。请先等待或发送 /stop,不要立即重复提交。':
|
||||
'Waiting for the model reply timed out and the task may still be running. Wait or send /stop before submitting it again.',
|
||||
'模型凭据缺失或已失效。请管理员检查模型配置后重试。':
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ const FAILURE_MESSAGES = Object.freeze({
|
|||
'机器人与 DeepSeek Harness 的接口不兼容。请管理员检查 Harness 地址并更新相关版本。',
|
||||
HARNESS_SERVICE:
|
||||
'DeepSeek Harness 暂时无法完成请求,请稍后重试。',
|
||||
SESSION_CREATE:
|
||||
'无法在当前工作区创建新会话。请检查机器人工作区是否为绝对路径、目录存在、且运行 Host 的账号可访问;改回可用工作区后重试。',
|
||||
MODEL_REPLY_TIMEOUT:
|
||||
'等待模型回复超时,任务可能仍在运行。请先等待或发送 /stop,不要立即重复提交。',
|
||||
MODEL_AUTH:
|
||||
|
|
@ -159,8 +161,13 @@ function providerFailureCode(error) {
|
|||
return mapped;
|
||||
}
|
||||
|
||||
function rpcMethod(error) {
|
||||
return typeof error?.method === 'string' ? error.method.trim() : '';
|
||||
}
|
||||
|
||||
function failureCode(error) {
|
||||
const code = typeof error?.code === 'string' ? error.code : '';
|
||||
const method = rpcMethod(error);
|
||||
const providerCode = providerFailureCode(error);
|
||||
if (providerCode) return providerCode;
|
||||
|
||||
|
|
@ -168,12 +175,12 @@ function failureCode(error) {
|
|||
if (isTransportProviderMessage(providerErrorMessage(error))) return 'MODEL_TRANSPORT';
|
||||
|
||||
if (code === 'harness-connect-failed') {
|
||||
return ['session.prompt', 'session.history'].includes(error?.method)
|
||||
return ['session.prompt', 'session.history'].includes(method)
|
||||
? 'HARNESS_RESULT_UNCERTAIN'
|
||||
: 'HARNESS_CONNECT';
|
||||
}
|
||||
if (code === 'harness-timeout') {
|
||||
return ['session.prompt', 'session.history'].includes(error?.method)
|
||||
return ['session.prompt', 'session.history'].includes(method)
|
||||
? 'HARNESS_RESULT_UNCERTAIN'
|
||||
: 'HARNESS_TIMEOUT';
|
||||
}
|
||||
|
|
@ -187,8 +194,15 @@ function failureCode(error) {
|
|||
return 'HARNESS_PROTOCOL';
|
||||
}
|
||||
if (code === 'harness-turn-failed') return 'INTERNAL_UNKNOWN';
|
||||
// Host RPC catch-all (common when session/preset/workspace ops fail without a typed code).
|
||||
// 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)) {
|
||||
if (method === 'workspace.create' || method === 'workspace.list') {
|
||||
return 'WORKSPACE_UNAVAILABLE';
|
||||
}
|
||||
if (method === 'session.create') return 'SESSION_CREATE';
|
||||
if (method.startsWith('agent-preset') || method.includes('preset')) {
|
||||
return 'PRESET_UNAVAILABLE';
|
||||
}
|
||||
return 'HARNESS_SERVICE';
|
||||
}
|
||||
if (code === 'model-empty-response') return 'MODEL_EMPTY_REPLY';
|
||||
|
|
@ -235,7 +249,20 @@ function inferredFailureReason(error) {
|
|||
?? safeFailureReason(error?.name);
|
||||
}
|
||||
|
||||
async function persistInternalUnknownDiagnostic(failure, error) {
|
||||
const PERSISTED_FAILURE_CODES = new Set([
|
||||
'INTERNAL_UNKNOWN',
|
||||
'HARNESS_SERVICE',
|
||||
'SESSION_CREATE',
|
||||
'WORKSPACE_UNAVAILABLE',
|
||||
'PRESET_UNAVAILABLE',
|
||||
'HARNESS_CONNECT',
|
||||
'HARNESS_TIMEOUT',
|
||||
'HARNESS_ACCESS',
|
||||
'HARNESS_PROTOCOL',
|
||||
'MODEL_TRANSPORT',
|
||||
]);
|
||||
|
||||
async function persistMessageFailureDiagnostic(failure, error) {
|
||||
try {
|
||||
const dir = join(homedir(), '.dsh', 'integrations', 'dsh-im-ops');
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
|
@ -251,10 +278,18 @@ async function persistInternalUnknownDiagnostic(failure, error) {
|
|||
messages: diagnosticMessages(error).map((text) => text.slice(0, 240)),
|
||||
};
|
||||
await writeFile(
|
||||
join(dir, 'last-internal-unknown.json'),
|
||||
join(dir, 'last-message-failure.json'),
|
||||
`${JSON.stringify(payload, null, 2)}\n`,
|
||||
{ encoding: 'utf8', mode: 0o600 },
|
||||
);
|
||||
// Keep the legacy filename for INTERNAL_UNKNOWN so older runbooks still work.
|
||||
if (failure.code === 'INTERNAL_UNKNOWN') {
|
||||
await writeFile(
|
||||
join(dir, 'last-internal-unknown.json'),
|
||||
`${JSON.stringify(payload, null, 2)}\n`,
|
||||
{ encoding: 'utf8', mode: 0o600 },
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Diagnostics must never mask the user-facing failure path.
|
||||
}
|
||||
|
|
@ -285,9 +320,10 @@ export function classifyMessageFailure(error, {
|
|||
&& userMessage.trim()
|
||||
? 'INPUT_INVALID'
|
||||
: classifiedCode;
|
||||
const methodReason = safeFailureReason(rpcMethod(error)?.replaceAll('.', '_'));
|
||||
const safeReason = code === 'INTERNAL_UNKNOWN'
|
||||
? (explicitReason ?? inferredFailureReason(error) ?? code)
|
||||
: (explicitReason ?? code);
|
||||
: (explicitReason ?? methodReason ?? code);
|
||||
return Object.freeze({
|
||||
code,
|
||||
reason: safeReason,
|
||||
|
|
@ -306,8 +342,8 @@ export function messageFailureText(failure) {
|
|||
export function setLastMessageFailure(status, error, options) {
|
||||
const failure = classifyMessageFailure(error, options);
|
||||
status.lastMessageError = failure;
|
||||
if (failure.code === 'INTERNAL_UNKNOWN') {
|
||||
void persistInternalUnknownDiagnostic(failure, error);
|
||||
if (PERSISTED_FAILURE_CODES.has(failure.code)) {
|
||||
void persistMessageFailureDiagnostic(failure, error);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@ test('PI_AI_ERROR with TLS or transport wording maps to MODEL_TRANSPORT', () =>
|
|||
test('RPC internal and UNKNOWN provider codes no longer fall through to INTERNAL_UNKNOWN', () => {
|
||||
assert.equal(classifyMessageFailure({
|
||||
code: 'internal', method: 'session.create',
|
||||
}, options).code, 'SESSION_CREATE');
|
||||
assert.equal(classifyMessageFailure({
|
||||
code: 'internal', method: 'workspace.create',
|
||||
}, options).code, 'WORKSPACE_UNAVAILABLE');
|
||||
assert.equal(classifyMessageFailure({
|
||||
code: 'internal', method: 'session.prompt',
|
||||
}, options).code, 'HARNESS_SERVICE');
|
||||
assert.equal(classifyMessageFailure({
|
||||
code: 'harness-turn-failed', providerCode: 'UNKNOWN',
|
||||
|
|
@ -102,6 +108,15 @@ test('RPC internal and UNKNOWN provider codes no longer fall through to INTERNAL
|
|||
assert.equal(plain.reason, 'ERROR');
|
||||
});
|
||||
|
||||
test('session.create failures tell operators to check the bot workspace path', () => {
|
||||
const failure = classifyMessageFailure({
|
||||
code: 'internal', method: 'session.create',
|
||||
}, options);
|
||||
assert.equal(failure.code, 'SESSION_CREATE');
|
||||
assert.equal(failure.reason, 'SESSION_CREATE');
|
||||
assert.match(failure.message, /工作区/);
|
||||
});
|
||||
|
||||
test('message failure text contains a safe code and traceable reference', () => {
|
||||
const failure = classifyMessageFailure(new Error('secret-shaped internal detail'), options);
|
||||
assert.deepEqual(failure, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue