mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-22 15:15:18 +00:00
fix(daemon): harden workspace memory failure diagnostics
This commit is contained in:
parent
d1cbf11abf
commit
ffceef9568
5 changed files with 101 additions and 24 deletions
|
|
@ -278,11 +278,15 @@ function workspaceMemoryErrorData(
|
|||
code: string,
|
||||
err: unknown,
|
||||
): { errorKind: string; details?: string } {
|
||||
const details = extractRememberErrorDetails(err);
|
||||
return {
|
||||
errorKind: code,
|
||||
...(details ? { details } : {}),
|
||||
};
|
||||
try {
|
||||
const details = extractRememberErrorDetails(err);
|
||||
return {
|
||||
errorKind: code,
|
||||
...(details ? { details } : {}),
|
||||
};
|
||||
} catch {
|
||||
return { errorKind: code };
|
||||
}
|
||||
}
|
||||
|
||||
function parseAcpLocalReadRootsEnv(
|
||||
|
|
@ -5835,6 +5839,7 @@ class QwenAgent implements Agent {
|
|||
if (err instanceof RequestError) {
|
||||
throw err;
|
||||
}
|
||||
debugLogger.error('Workspace memory remember failed:', err);
|
||||
if (childSignal.aborted) {
|
||||
throw new RequestError(
|
||||
-32099,
|
||||
|
|
@ -5905,6 +5910,7 @@ class QwenAgent implements Agent {
|
|||
if (err instanceof RequestError) {
|
||||
throw err;
|
||||
}
|
||||
debugLogger.error('Workspace memory forget failed:', err);
|
||||
if (childSignal.aborted) {
|
||||
throw new RequestError(
|
||||
-32099,
|
||||
|
|
@ -5962,6 +5968,7 @@ class QwenAgent implements Agent {
|
|||
if (err instanceof RequestError) {
|
||||
throw err;
|
||||
}
|
||||
debugLogger.error('Workspace memory dream failed:', err);
|
||||
if (childSignal.aborted) {
|
||||
throw new RequestError(-32099, 'Workspace memory dream timed out', {
|
||||
errorKind: 'dream_timeout',
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ describe('extractRememberErrorCode', () => {
|
|||
cause: { code: 'remember_timeout' },
|
||||
}),
|
||||
).toBe('remember_timeout');
|
||||
expect(
|
||||
extractRememberErrorCode({
|
||||
cause: { cause: { code: 'remember_path_escape' } },
|
||||
}),
|
||||
).toBe('remember_path_escape');
|
||||
expect(extractRememberErrorCode(new Error('boom'))).toBe('remember_failed');
|
||||
expect(extractRememberErrorCode(new Error('boom'), 'forget_failed')).toBe(
|
||||
'forget_failed',
|
||||
|
|
@ -90,6 +95,18 @@ describe('extractRememberErrorDetails', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('redacts credentials with hidden separators inside token values', () => {
|
||||
for (const separator of ['\u200b', '\u2060', '\u2064']) {
|
||||
const details = extractRememberErrorDetails(
|
||||
new Error(`Authorization: Bearer secret${separator}token-value`),
|
||||
);
|
||||
|
||||
expect(details).toBe('Authorization: <redacted>');
|
||||
expect(details).not.toContain('secret');
|
||||
expect(details).not.toContain('token-value');
|
||||
}
|
||||
});
|
||||
|
||||
it('normalizes line separators before redacting credentials', () => {
|
||||
for (const separator of ['\u2028', '\u2029']) {
|
||||
const details = extractRememberErrorDetails(
|
||||
|
|
@ -101,6 +118,26 @@ describe('extractRememberErrorDetails', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('normalizes bidi isolation characters before redacting credentials', () => {
|
||||
for (const separator of ['\u2066', '\u2067', '\u2068', '\u2069']) {
|
||||
const details = extractRememberErrorDetails(
|
||||
new Error(`Authorization: Bearer${separator}secret-token-value`),
|
||||
);
|
||||
|
||||
expect(details).toBe('Authorization: <redacted>');
|
||||
expect(details).not.toContain('secret-token-value');
|
||||
}
|
||||
});
|
||||
|
||||
it('normalizes BOM before redacting credentials', () => {
|
||||
const details = extractRememberErrorDetails(
|
||||
new Error('Authorization: Bearer\ufeffsecret-token-value'),
|
||||
);
|
||||
|
||||
expect(details).toBe('Authorization: <redacted>');
|
||||
expect(details).not.toContain('secret-token-value');
|
||||
});
|
||||
|
||||
it('sanitizes control characters', () => {
|
||||
expect(extractRememberErrorDetails(new Error('line1\nline2\ttab'))).toBe(
|
||||
'line1 line2 tab',
|
||||
|
|
@ -133,4 +170,13 @@ describe('extractRememberErrorDetails', () => {
|
|||
expect(details).toMatch(/^x+\.{3} \[truncated\]$/);
|
||||
expect(details).toHaveLength(1000);
|
||||
});
|
||||
|
||||
it('does not split surrogate pairs when capping long details', () => {
|
||||
const details = extractRememberErrorDetails(
|
||||
new Error(`${'x'.repeat(984)}${'😀'.repeat(100)}`),
|
||||
);
|
||||
|
||||
expect(details).toBe(`${'x'.repeat(984)}... [truncated]`);
|
||||
expect(details).toHaveLength(999);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,21 +24,33 @@ function errorCodeFromRecord(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function rawRememberErrorCode(
|
||||
err: unknown,
|
||||
seen: WeakSet<object>,
|
||||
depth: number,
|
||||
): string | undefined {
|
||||
if (depth > MAX_REMEMBER_ERROR_CAUSE_DEPTH) return undefined;
|
||||
if (!err || typeof err !== 'object') return undefined;
|
||||
if (seen.has(err)) return undefined;
|
||||
seen.add(err);
|
||||
|
||||
const record = err as Record<string, unknown>;
|
||||
const direct = errorCodeFromRecord(record);
|
||||
if (direct) return direct;
|
||||
|
||||
const cause = record['cause'];
|
||||
if (cause != null) {
|
||||
return rawRememberErrorCode(cause, seen, depth + 1);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extractRememberErrorCode(
|
||||
err: unknown,
|
||||
fallback = 'remember_failed',
|
||||
): string {
|
||||
if (err && typeof err === 'object') {
|
||||
const record = err as Record<string, unknown>;
|
||||
const direct = errorCodeFromRecord(record);
|
||||
if (direct) return direct;
|
||||
const cause = record['cause'];
|
||||
if (cause && typeof cause === 'object') {
|
||||
const causedBy = errorCodeFromRecord(cause as Record<string, unknown>);
|
||||
if (causedBy) return causedBy;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
return rawRememberErrorCode(err, new WeakSet<object>(), 0) ?? fallback;
|
||||
}
|
||||
|
||||
function detailFromRecord(
|
||||
|
|
@ -112,16 +124,19 @@ function replaceControlChars(details: string): string {
|
|||
}
|
||||
|
||||
function sanitizeRememberErrorDetails(details: string): string | undefined {
|
||||
const normalized = redactLogCredentials(replaceControlChars(details)).trim();
|
||||
const redacted = redactLogCredentials(details);
|
||||
const normalized = redactLogCredentials(replaceControlChars(redacted)).trim();
|
||||
if (!normalized) return undefined;
|
||||
if (normalized.length <= MAX_REMEMBER_ERROR_DETAILS_CHARS) {
|
||||
return normalized;
|
||||
}
|
||||
const truncationSuffix = '... [truncated]';
|
||||
return `${normalized.slice(
|
||||
0,
|
||||
MAX_REMEMBER_ERROR_DETAILS_CHARS - truncationSuffix.length,
|
||||
)}${truncationSuffix}`;
|
||||
let cutPoint = MAX_REMEMBER_ERROR_DETAILS_CHARS - truncationSuffix.length;
|
||||
const codeUnit = normalized.charCodeAt(cutPoint - 1);
|
||||
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
||||
cutPoint -= 1;
|
||||
}
|
||||
return `${normalized.slice(0, cutPoint)}${truncationSuffix}`;
|
||||
}
|
||||
|
||||
export function extractRememberErrorDetails(err: unknown): string | undefined {
|
||||
|
|
|
|||
|
|
@ -865,6 +865,7 @@ describe('workspace memory remember routes', () => {
|
|||
)
|
||||
.mockRejectedValueOnce({
|
||||
data: { errorKind: 'managed_memory_unavailable' },
|
||||
message: 'internal managed memory config path',
|
||||
})
|
||||
.mockRejectedValueOnce({
|
||||
data: { errorKind: 'remember_timeout' },
|
||||
|
|
|
|||
|
|
@ -187,12 +187,20 @@ function createTaskError(
|
|||
kind: WorkspaceMemoryTaskKind,
|
||||
err: unknown,
|
||||
): WorkspaceMemoryTaskBaseSnapshot['error'] {
|
||||
const details = extractRememberErrorDetails(err);
|
||||
return {
|
||||
const error: WorkspaceMemoryTaskBaseSnapshot['error'] = {
|
||||
code,
|
||||
message: publicErrorMessage(code, kind),
|
||||
...(details ? { details } : {}),
|
||||
};
|
||||
if (code === 'managed_memory_unavailable') return error;
|
||||
try {
|
||||
const details = extractRememberErrorDetails(err);
|
||||
return {
|
||||
...error,
|
||||
...(details ? { details } : {}),
|
||||
};
|
||||
} catch {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceRememberTaskLane {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue