dsh-client-auto-continue 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/README.zh.md +11 -3
- package/docs/auto-continue-workflow-icons.LICENSE.txt +40 -0
- package/docs/auto-continue-workflow.archify.json +155 -0
- package/docs/auto-continue-workflow.en.archify.json +155 -0
- package/docs/auto-continue-workflow.en.html +14844 -0
- package/docs/auto-continue-workflow.en.svg +530 -0
- package/docs/auto-continue-workflow.html +14844 -0
- package/docs/auto-continue-workflow.svg +530 -0
- package/docs/screenshots/02-settings-card.png +0 -0
- package/lib/client.js +760 -348
- package/lib/client.js.map +2 -2
- package/lib/index.js +3 -3
- package/lib/types/client/locales.d.ts +17 -0
- package/package.json +2 -2
- package/scripts/apply-workflow-icons.mjs +165 -0
- package/src/client/locales.ts +36 -2
- package/src/client/settings-card.tsx +420 -265
- package/src/client/styles.ts +259 -17
- package/src/index.ts +5 -5
package/lib/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import z2 from "@deepseek-ai/schemastery";
|
|
3
|
-
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
3
|
|
|
5
4
|
// node_modules/@deepseek-ai/dsh-llm/lib/index.js
|
|
6
5
|
import { createRequire } from "node:module";
|
|
@@ -949,6 +948,7 @@ ${event.data.arguments}`;
|
|
|
949
948
|
|
|
950
949
|
// src/index.ts
|
|
951
950
|
var AUTO_CONTINUE_NS = "auto-continue";
|
|
951
|
+
var SETTINGS_NS = AUTO_CONTINUE_NS;
|
|
952
952
|
var AutoContinueSchema = z2.object({
|
|
953
953
|
/** Active browser/UI locale mirrored by the client. */
|
|
954
954
|
locale: z2.string().default("zh"),
|
|
@@ -1005,7 +1005,7 @@ var AutoContinueSchema = z2.object({
|
|
|
1005
1005
|
});
|
|
1006
1006
|
function apply(ctx) {
|
|
1007
1007
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
1008
|
-
settingsCtx.settings.register(
|
|
1008
|
+
settingsCtx.settings.register(SETTINGS_NS, AutoContinueSchema, {
|
|
1009
1009
|
applies: "live"
|
|
1010
1010
|
});
|
|
1011
1011
|
});
|
|
@@ -1014,7 +1014,7 @@ function apply(ctx) {
|
|
|
1014
1014
|
if (runnerRef !== void 0) runnerRef.dispose();
|
|
1015
1015
|
const runner = new AutoContinueRunner(
|
|
1016
1016
|
engineCtx,
|
|
1017
|
-
() => resolveConfig(engineCtx.settings.get(
|
|
1017
|
+
() => resolveConfig(engineCtx.settings.get(SETTINGS_NS))
|
|
1018
1018
|
);
|
|
1019
1019
|
runnerRef = runner;
|
|
1020
1020
|
const sseClients = /* @__PURE__ */ new Set();
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
/** 简体中文词典(键集的事实来源)。 */
|
|
2
2
|
export declare const zh: {
|
|
3
3
|
'card.title': string;
|
|
4
|
+
'card.eyebrow': string;
|
|
4
5
|
'card.description': string;
|
|
6
|
+
'flow.interrupted': string;
|
|
7
|
+
'flow.grace': string;
|
|
8
|
+
'flow.continue': string;
|
|
9
|
+
'repo.star': string;
|
|
10
|
+
'repo.aria': string;
|
|
11
|
+
'section.handoff.title': string;
|
|
12
|
+
'section.handoff.description': string;
|
|
13
|
+
'section.safety.title': string;
|
|
14
|
+
'section.safety.description': string;
|
|
15
|
+
'section.recovery.title': string;
|
|
16
|
+
'section.recovery.description': string;
|
|
17
|
+
'section.loop.title': string;
|
|
18
|
+
'section.loop.description': string;
|
|
19
|
+
'section.live.title': string;
|
|
20
|
+
'section.live.description': string;
|
|
5
21
|
'field.paused': string;
|
|
6
22
|
'field.pausedHint': string;
|
|
7
23
|
'field.continueText': string;
|
|
@@ -36,6 +52,7 @@ export declare const zh: {
|
|
|
36
52
|
'field.classifyHint': string;
|
|
37
53
|
'field.retryableErrorPatterns': string;
|
|
38
54
|
'field.retryableErrorPatternsHint': string;
|
|
55
|
+
'field.retryableErrorPatternsPlaceholder': string;
|
|
39
56
|
'field.backoffFactor': string;
|
|
40
57
|
'field.backoffFactorHint': string;
|
|
41
58
|
'field.backoffMaxMs': string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-client-auto-continue",
|
|
3
3
|
"description": "DSH Web UI plugin: automatically sends a localized continue prompt when a request is interrupted by network errors or other non-human causes",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"build": "node build.mjs && tsc -p tsconfig.build.json",
|
|
47
47
|
"watch": "node build.mjs --watch",
|
|
48
48
|
"typecheck": "tsc --noEmit",
|
|
49
|
-
"test": "node tests/simulate-host.mjs && node tests/simulate-client-loader.mjs && node tests/prepare-github-package.mjs",
|
|
49
|
+
"test": "node tests/simulate-host.mjs && node tests/settings-host-compat.mjs && node tests/simulate-client-loader.mjs && node tests/settings-card-ui.mjs && node tests/prepare-github-package.mjs",
|
|
50
50
|
"prepack": "npm run build"
|
|
51
51
|
},
|
|
52
52
|
"keywords": [
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const artifactPath = process.argv[2]
|
|
7
|
+
? path.resolve(process.cwd(), process.argv[2])
|
|
8
|
+
: path.resolve(scriptDir, '../docs/auto-continue-workflow.html');
|
|
9
|
+
|
|
10
|
+
// Lucide icon geometry uses viewBox="0 0 24 24". The surrounding Archify
|
|
11
|
+
// semantic class continues to own color, stroke width, theme, and interaction.
|
|
12
|
+
// Source and license: https://lucide.dev/ and
|
|
13
|
+
// docs/auto-continue-workflow-icons.LICENSE.txt.
|
|
14
|
+
const icons = {
|
|
15
|
+
turn_end: {
|
|
16
|
+
name: 'radio',
|
|
17
|
+
elements: [
|
|
18
|
+
'<path d="M16.247 7.761a6 6 0 0 1 0 8.478"/>',
|
|
19
|
+
'<path d="M19.075 4.933a10 10 0 0 1 0 14.134"/>',
|
|
20
|
+
'<path d="M4.925 19.067a10 10 0 0 1 0-14.134"/>',
|
|
21
|
+
'<path d="M7.753 16.239a6 6 0 0 1 0-8.478"/>',
|
|
22
|
+
'<circle cx="12" cy="12" r="2"/>',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
classify_failure: {
|
|
26
|
+
name: 'scan-search',
|
|
27
|
+
elements: [
|
|
28
|
+
'<path d="M3 7V5a2 2 0 0 1 2-2h2"/>',
|
|
29
|
+
'<path d="M17 3h2a2 2 0 0 1 2 2v2"/>',
|
|
30
|
+
'<path d="M21 17v2a2 2 0 0 1-2 2h-2"/>',
|
|
31
|
+
'<path d="M7 21H5a2 2 0 0 1-2-2v-2"/>',
|
|
32
|
+
'<circle cx="12" cy="12" r="3"/>',
|
|
33
|
+
'<path d="m16 16-1.9-1.9"/>',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
safety_gate: {
|
|
37
|
+
name: 'shield-check',
|
|
38
|
+
elements: [
|
|
39
|
+
'<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
|
40
|
+
'<path d="m9 12 2 2 4-4"/>',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
grace_wait: {
|
|
44
|
+
name: 'timer',
|
|
45
|
+
elements: [
|
|
46
|
+
'<line x1="10" x2="14" y1="2" y2="2"/>',
|
|
47
|
+
'<line x1="12" x2="15" y1="14" y2="11"/>',
|
|
48
|
+
'<circle cx="12" cy="14" r="8"/>',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
compose_prompt: {
|
|
52
|
+
name: 'message-square-text',
|
|
53
|
+
elements: [
|
|
54
|
+
'<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/>',
|
|
55
|
+
'<path d="M7 11h10"/>',
|
|
56
|
+
'<path d="M7 15h6"/>',
|
|
57
|
+
'<path d="M7 7h8"/>',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
agent_followup: {
|
|
61
|
+
name: 'send',
|
|
62
|
+
elements: [
|
|
63
|
+
'<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/>',
|
|
64
|
+
'<path d="m21.854 2.147-10.94 10.939"/>',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
recovery_feedback: {
|
|
68
|
+
name: 'circle-check-big',
|
|
69
|
+
elements: [
|
|
70
|
+
'<path d="M21.801 10A10 10 0 1 1 17 3.335"/>',
|
|
71
|
+
'<path d="m9 11 3 3L22 4"/>',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
agent_cancel: {
|
|
75
|
+
name: 'circle-stop',
|
|
76
|
+
elements: [
|
|
77
|
+
'<circle cx="12" cy="12" r="10"/>',
|
|
78
|
+
'<rect x="9" y="9" width="6" height="6" rx="1"/>',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
manual_stop: {
|
|
82
|
+
name: 'hand',
|
|
83
|
+
elements: [
|
|
84
|
+
'<path d="M18 11V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2"/>',
|
|
85
|
+
'<path d="M14 10V4a2 2 0 0 0-2-2 2 2 0 0 0-2 2v2"/>',
|
|
86
|
+
'<path d="M10 10.5V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2v8"/>',
|
|
87
|
+
'<path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/>',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const ICON_SCALE = 0.4583333333;
|
|
93
|
+
const ICON_RENDERED_SIZE = 24 * ICON_SCALE;
|
|
94
|
+
|
|
95
|
+
function compactNumber(value) {
|
|
96
|
+
return Number(value.toFixed(4)).toString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function replaceTextY(nodeHtml, selectorPattern, y) {
|
|
100
|
+
const pattern = new RegExp(`(<text ${selectorPattern}[^>]*\\sy=")[^"]+("[^>]*>)`);
|
|
101
|
+
if (!pattern.test(nodeHtml)) return nodeHtml;
|
|
102
|
+
return nodeHtml.replace(pattern, `$1${compactNumber(y)}$2`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let html = fs.readFileSync(artifactPath, 'utf8');
|
|
106
|
+
let applied = 0;
|
|
107
|
+
|
|
108
|
+
for (const [nodeId, icon] of Object.entries(icons)) {
|
|
109
|
+
const nodeStart = html.indexOf(`<g id="node-${nodeId}"`);
|
|
110
|
+
if (nodeStart === -1) throw new Error(`Missing workflow node: ${nodeId}`);
|
|
111
|
+
|
|
112
|
+
const nextNode = html.indexOf('<g id="node-', nodeStart + 1);
|
|
113
|
+
const edgeLabels = html.indexOf('<!-- Edge labels -->', nodeStart);
|
|
114
|
+
const nodeEnd = nextNode === -1 ? edgeLabels : Math.min(nextNode, edgeLabels);
|
|
115
|
+
if (nodeEnd === -1) throw new Error(`Missing workflow node boundary: ${nodeId}`);
|
|
116
|
+
|
|
117
|
+
let nodeHtml = html.slice(nodeStart, nodeEnd);
|
|
118
|
+
const rectMatch = nodeHtml.match(/<rect x="([^"]+)" y="([^"]+)" width="([^"]+)" height="([^"]+)"/);
|
|
119
|
+
if (!rectMatch) throw new Error(`Missing workflow node bounds: ${nodeId}`);
|
|
120
|
+
|
|
121
|
+
const [, rawX, rawY, rawWidth, rawHeight] = rectMatch;
|
|
122
|
+
const x = Number(rawX);
|
|
123
|
+
const y = Number(rawY);
|
|
124
|
+
const width = Number(rawWidth);
|
|
125
|
+
const height = Number(rawHeight);
|
|
126
|
+
const iconX = x + (width - ICON_RENDERED_SIZE) / 2;
|
|
127
|
+
const iconY = y + 5;
|
|
128
|
+
|
|
129
|
+
const sigilStart = nodeHtml.indexOf('<g aria-hidden="true" data-semantic-sigil=');
|
|
130
|
+
const nodeLabel = nodeHtml.indexOf('<text data-node-label=');
|
|
131
|
+
if (sigilStart === -1 || nodeLabel === -1 || sigilStart > nodeLabel) {
|
|
132
|
+
throw new Error(`Missing semantic sigil for node: ${nodeId}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const openEnd = nodeHtml.indexOf('>', sigilStart);
|
|
136
|
+
const closeStart = nodeHtml.indexOf('</g>', openEnd);
|
|
137
|
+
if (openEnd === -1 || closeStart === -1 || closeStart > nodeLabel) {
|
|
138
|
+
throw new Error(`Malformed semantic sigil for node: ${nodeId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let openTag = nodeHtml.slice(sigilStart, openEnd + 1)
|
|
142
|
+
.replace(/\sdata-lucide-icon="[^"]*"/, '')
|
|
143
|
+
.replace(/\sdata-icon-layout="[^"]*"/, '')
|
|
144
|
+
.replace(/transform="[^"]*"/, `transform="translate(${compactNumber(iconX)} ${compactNumber(iconY)}) scale(${ICON_SCALE})"`)
|
|
145
|
+
.replace(/>$/, ` data-lucide-icon="${icon.name}" data-icon-layout="stacked">`);
|
|
146
|
+
const body = icon.elements.map((element) => ` ${element}`).join('\n');
|
|
147
|
+
nodeHtml = `${nodeHtml.slice(0, sigilStart)}${openTag}\n${body}\n ${nodeHtml.slice(closeStart)}`;
|
|
148
|
+
|
|
149
|
+
// Stack icon, title, detail, and optional tag on distinct baselines.
|
|
150
|
+
// Standard nodes are 52px high; tagged nodes use the extra 16px row.
|
|
151
|
+
nodeHtml = replaceTextY(nodeHtml, 'data-node-label="[^"]*"', y + 29);
|
|
152
|
+
nodeHtml = replaceTextY(nodeHtml, 'data-detail="context"', y + (height > 52 ? 46 : 44));
|
|
153
|
+
nodeHtml = replaceTextY(nodeHtml, 'data-detail="fine"', y + 61);
|
|
154
|
+
|
|
155
|
+
html = `${html.slice(0, nodeStart)}${nodeHtml}${html.slice(nodeEnd)}`;
|
|
156
|
+
applied += 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const attribution = '<!-- Node glyphs: Lucide Icons; see auto-continue-workflow-icons.LICENSE.txt -->';
|
|
160
|
+
if (!html.includes(attribution)) {
|
|
161
|
+
html = html.replace(' <!-- Nodes -->', ` <!-- Nodes -->\n ${attribution}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fs.writeFileSync(artifactPath, html);
|
|
165
|
+
console.log(`Applied ${applied} Lucide workflow icons to ${artifactPath}`);
|
package/src/client/locales.ts
CHANGED
|
@@ -8,7 +8,23 @@ import { LOCALIZED_TEXT_DEFAULTS } from '../shared/core.ts';
|
|
|
8
8
|
/** 简体中文词典(键集的事实来源)。 */
|
|
9
9
|
export const zh = {
|
|
10
10
|
'card.title': '自动继续',
|
|
11
|
-
'card.
|
|
11
|
+
'card.eyebrow': '中断接力器',
|
|
12
|
+
'card.description': '接住意外中断的请求,等现场稳定下来,再把对话交还给 Agent。',
|
|
13
|
+
'flow.interrupted': '检测中断',
|
|
14
|
+
'flow.grace': '安全等待',
|
|
15
|
+
'flow.continue': '发送继续',
|
|
16
|
+
'repo.star': '去 GitHub 点 Star',
|
|
17
|
+
'repo.aria': '打开 dsh-auto-continue 的 GitHub 开源地址并点 Star',
|
|
18
|
+
'section.handoff.title': '接力方式',
|
|
19
|
+
'section.handoff.description': '决定中断后说什么,以及什么时候暂时停手。',
|
|
20
|
+
'section.safety.title': '安全节奏',
|
|
21
|
+
'section.safety.description': '先确认上一步,再控制等待与连续续跑的边界。',
|
|
22
|
+
'section.recovery.title': '恢复雷达',
|
|
23
|
+
'section.recovery.description': '扫描掉线会话,识别可恢复错误并逐步退避。',
|
|
24
|
+
'section.loop.title': '循环断路器',
|
|
25
|
+
'section.loop.description': '发现空转时及时换路,而不是让 Agent 原地消耗。',
|
|
26
|
+
'section.live.title': '现场状态',
|
|
27
|
+
'section.live.description': '看看今天接住了多少次,以及哪些会话还在暂停。',
|
|
12
28
|
'field.paused': '暂停自动继续',
|
|
13
29
|
'field.pausedHint': '全局暂停: 实时与扫描都不会再自动发送, 已排队的待发送也会取消。',
|
|
14
30
|
'field.continueText': '继续文本',
|
|
@@ -43,6 +59,7 @@ export const zh = {
|
|
|
43
59
|
'field.classifyHint': '仅自动恢复临时性错误(网络/超时/5xx 等); 认证/余额/模型不存在等永久性错误跳过并通知。',
|
|
44
60
|
'field.retryableErrorPatterns': '自定义可恢复错误',
|
|
45
61
|
'field.retryableErrorPatternsHint': '每行一个大小写不敏感的普通文本片段; 命中错误码、HTTP 状态或消息时覆盖内置分类。请只填 provider 稳定且足够具体的文案, 过宽会重复请求。',
|
|
62
|
+
'field.retryableErrorPatternsPlaceholder': '例如:Upstream rejected the request as invalid',
|
|
46
63
|
'field.backoffFactor': '退避系数',
|
|
47
64
|
'field.backoffFactorHint': '连续失败时冷却间隔的倍率(如 2 表示 20s→40s→80s 递增)。',
|
|
48
65
|
'field.backoffMaxMs': '最大退避间隔 (ms)',
|
|
@@ -101,7 +118,23 @@ export type SettingsCardKey = keyof typeof zh;
|
|
|
101
118
|
/** English dictionary, checked complete against the zh key set. */
|
|
102
119
|
export const en: Record<SettingsCardKey, string> = {
|
|
103
120
|
'card.title': 'Auto continue',
|
|
104
|
-
'card.
|
|
121
|
+
'card.eyebrow': 'Continuity relay',
|
|
122
|
+
'card.description': 'Catches an interrupted request, waits for things to settle, then hands the thread back to the agent.',
|
|
123
|
+
'flow.interrupted': 'Interrupted',
|
|
124
|
+
'flow.grace': 'Safe pause',
|
|
125
|
+
'flow.continue': 'Continue',
|
|
126
|
+
'repo.star': 'Star on GitHub',
|
|
127
|
+
'repo.aria': 'Open the dsh-auto-continue repository on GitHub and star it',
|
|
128
|
+
'section.handoff.title': 'The handoff',
|
|
129
|
+
'section.handoff.description': 'Choose what gets sent after an interruption and when the relay should stand down.',
|
|
130
|
+
'section.safety.title': 'Safety rhythm',
|
|
131
|
+
'section.safety.description': 'Confirm the previous step, then set the boundaries for waiting and repeated resumes.',
|
|
132
|
+
'section.recovery.title': 'Recovery radar',
|
|
133
|
+
'section.recovery.description': 'Find dropped sessions, recognize recoverable errors, and back off gracefully.',
|
|
134
|
+
'section.loop.title': 'Loop breaker',
|
|
135
|
+
'section.loop.description': 'Spot a spinning agent and route it forward before it burns time in place.',
|
|
136
|
+
'section.live.title': 'Live signal',
|
|
137
|
+
'section.live.description': 'See what the relay caught today and which sessions are still paused.',
|
|
105
138
|
'field.paused': 'Pause auto-continue',
|
|
106
139
|
'field.pausedHint': 'Globally pause: no live or scan auto-send fires, and queued pending sends are cancelled.',
|
|
107
140
|
'field.continueText': 'Continue text',
|
|
@@ -136,6 +169,7 @@ export const en: Record<SettingsCardKey, string> = {
|
|
|
136
169
|
'field.classifyHint': 'Auto-resume transient failures only (network/timeout/5xx…); auth, balance and model errors are skipped and notified.',
|
|
137
170
|
'field.retryableErrorPatterns': 'Custom retryable errors',
|
|
138
171
|
'field.retryableErrorPatternsHint': 'One case-insensitive literal per line. A match in the error code, HTTP status, or message overrides built-in classification. Use only stable, provider-specific text; broad matches can repeat requests.',
|
|
172
|
+
'field.retryableErrorPatternsPlaceholder': 'For example: Upstream rejected the request as invalid',
|
|
139
173
|
'field.backoffFactor': 'Backoff factor',
|
|
140
174
|
'field.backoffFactorHint': 'Cooldown multiplier per consecutive failure (2 = 20s→40s→80s…).',
|
|
141
175
|
'field.backoffMaxMs': 'Max backoff (ms)',
|