@vellumai/assistant 0.3.2 → 0.3.4
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 +82 -21
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
- package/src/__tests__/app-git-history.test.ts +22 -27
- package/src/__tests__/app-git-service.test.ts +44 -78
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +1267 -93
- package/src/__tests__/channel-approval.test.ts +2 -0
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +130 -1
- package/src/__tests__/channel-guardian.test.ts +371 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/daemon-server-session-init.test.ts +5 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
- package/src/__tests__/handlers-telegram-config.test.ts +82 -0
- package/src/__tests__/handlers-twilio-config.test.ts +738 -5
- package/src/__tests__/ingress-url-consistency.test.ts +64 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/run-orchestrator.test.ts +1 -1
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-process-bridge.test.ts +2 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/calls/twilio-config.ts +10 -1
- package/src/calls/twilio-rest.ts +70 -0
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +6 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +193 -17
- package/src/daemon/handlers/sessions.ts +5 -3
- package/src/daemon/handlers/skills.ts +60 -17
- package/src/daemon/ipc-contract-inventory.json +4 -0
- package/src/daemon/ipc-contract.ts +16 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +9 -14
- package/src/daemon/session-process.ts +20 -3
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session-slash.ts +50 -2
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +20 -3
- package/src/index.ts +1 -23
- package/src/memory/app-git-service.ts +24 -0
- package/src/memory/app-store.ts +0 -21
- package/src/memory/channel-delivery-store.ts +74 -3
- package/src/memory/channel-guardian-store.ts +54 -26
- package/src/memory/conversation-key-store.ts +20 -0
- package/src/memory/conversation-store.ts +14 -2
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -1995
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +6 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +28 -9
- package/src/runtime/routes/channel-routes.ts +279 -100
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/clawhub.ts +6 -2
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/subagent/manager.ts +4 -1
- package/src/subagent/types.ts +2 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +75 -127
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { RiskLevel } from '../../permissions/types.js';
|
|
5
2
|
import type { ToolDefinition } from '../../providers/types.js';
|
|
3
|
+
import { parseFrontmatterFields } from '../../skills/frontmatter.js';
|
|
6
4
|
import { createManagedSkill } from '../../skills/managed-store.js';
|
|
7
|
-
import {
|
|
5
|
+
import { fetchCatalogEntries, fetchSkillContent, checkVellumSkill } from '../../skills/vellum-catalog-remote.js';
|
|
8
6
|
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
13
|
-
|
|
14
|
-
function getVellumSkillsDir(): string {
|
|
15
|
-
return join(import.meta.dir, '..', '..', 'config', 'vellum-skills');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface CatalogEntry {
|
|
8
|
+
export interface CatalogEntry {
|
|
19
9
|
id: string;
|
|
20
10
|
name: string;
|
|
21
11
|
description: string;
|
|
@@ -23,88 +13,83 @@ interface CatalogEntry {
|
|
|
23
13
|
includes?: string[];
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
const skillFilePath = join(directory, 'SKILL.md');
|
|
28
|
-
if (!existsSync(skillFilePath)) return null;
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const stat = statSync(skillFilePath);
|
|
32
|
-
if (!stat.isFile()) return null;
|
|
33
|
-
|
|
34
|
-
const content = readFileSync(skillFilePath, 'utf-8');
|
|
35
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
36
|
-
if (!match) return null;
|
|
37
|
-
|
|
38
|
-
const fields: Record<string, string> = {};
|
|
39
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
40
|
-
const trimmed = line.trim();
|
|
41
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
42
|
-
const separatorIndex = trimmed.indexOf(':');
|
|
43
|
-
if (separatorIndex === -1) continue;
|
|
44
|
-
|
|
45
|
-
const key = trimmed.slice(0, separatorIndex).trim();
|
|
46
|
-
let value = trimmed.slice(separatorIndex + 1).trim();
|
|
47
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
48
|
-
value = value.slice(1, -1);
|
|
49
|
-
}
|
|
50
|
-
fields[key] = value;
|
|
51
|
-
}
|
|
16
|
+
export { fetchCatalogEntries as listCatalogEntries, checkVellumSkill };
|
|
52
17
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const parsed = JSON.parse(metadataRaw);
|
|
62
|
-
if (parsed?.vellum?.emoji) {
|
|
63
|
-
emoji = parsed.vellum.emoji as string;
|
|
64
|
-
}
|
|
65
|
-
} catch {
|
|
66
|
-
// ignore malformed metadata
|
|
67
|
-
}
|
|
68
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* Install a skill from the vellum-skills catalog by ID.
|
|
20
|
+
* Fetches SKILL.md from GitHub (with bundled fallback) and creates a managed skill.
|
|
21
|
+
* Returns { success, skillName, error }.
|
|
22
|
+
*/
|
|
23
|
+
export async function installFromVellumCatalog(skillId: string, options?: { overwrite?: boolean }): Promise<{ success: boolean; skillName?: string; error?: string }> {
|
|
24
|
+
const trimmedId = skillId.trim();
|
|
69
25
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
26
|
+
// Verify skill exists in catalog
|
|
27
|
+
const exists = await checkVellumSkill(trimmedId);
|
|
28
|
+
if (!exists) {
|
|
29
|
+
return { success: false, error: `Skill "${trimmedId}" not found in the Vellum catalog` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fetch SKILL.md content (remote with bundled fallback)
|
|
33
|
+
const content = await fetchSkillContent(trimmedId);
|
|
34
|
+
if (!content) {
|
|
35
|
+
return { success: false, error: `Skill "${trimmedId}" SKILL.md not found` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = parseFrontmatterFields(content);
|
|
39
|
+
if (!parsed) {
|
|
40
|
+
return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { fields, body: bodyMarkdown } = parsed;
|
|
44
|
+
|
|
45
|
+
const name = fields.name?.trim();
|
|
46
|
+
const description = fields.description?.trim();
|
|
47
|
+
if (!name || !description) {
|
|
48
|
+
return { success: false, error: `Skill "${trimmedId}" has invalid SKILL.md (missing name or description)` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let emoji: string | undefined;
|
|
52
|
+
const metadataRaw = fields.metadata?.trim();
|
|
53
|
+
if (metadataRaw) {
|
|
54
|
+
try {
|
|
55
|
+
const metaObj = JSON.parse(metadataRaw);
|
|
56
|
+
if (metaObj?.vellum?.emoji) {
|
|
57
|
+
emoji = metaObj.vellum.emoji as string;
|
|
81
58
|
}
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore malformed metadata
|
|
82
61
|
}
|
|
83
|
-
|
|
84
|
-
return { id: basename(directory), name, description, emoji, includes };
|
|
85
|
-
} catch (err) {
|
|
86
|
-
log.warn({ err, directory }, 'Failed to read catalog entry');
|
|
87
|
-
return null;
|
|
88
62
|
}
|
|
89
|
-
}
|
|
90
63
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
64
|
+
let includes: string[] | undefined;
|
|
65
|
+
const includesRaw = fields.includes?.trim();
|
|
66
|
+
if (includesRaw) {
|
|
67
|
+
try {
|
|
68
|
+
const includesObj = JSON.parse(includesRaw);
|
|
69
|
+
if (Array.isArray(includesObj) && includesObj.every((item: unknown) => typeof item === 'string')) {
|
|
70
|
+
const filtered = (includesObj as string[]).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
71
|
+
if (filtered.length > 0) includes = filtered;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore malformed includes
|
|
102
75
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
76
|
+
}
|
|
77
|
+
const result = createManagedSkill({
|
|
78
|
+
id: trimmedId,
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
81
|
+
bodyMarkdown,
|
|
82
|
+
emoji,
|
|
83
|
+
includes,
|
|
84
|
+
overwrite: options?.overwrite ?? true,
|
|
85
|
+
addToIndex: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!result.created) {
|
|
89
|
+
return { success: false, error: result.error };
|
|
105
90
|
}
|
|
106
91
|
|
|
107
|
-
return
|
|
92
|
+
return { success: true, skillName: trimmedId };
|
|
108
93
|
}
|
|
109
94
|
|
|
110
95
|
class VellumSkillsCatalogTool implements Tool {
|
|
@@ -144,7 +129,7 @@ class VellumSkillsCatalogTool implements Tool {
|
|
|
144
129
|
|
|
145
130
|
switch (action) {
|
|
146
131
|
case 'list': {
|
|
147
|
-
const entries =
|
|
132
|
+
const entries = await fetchCatalogEntries();
|
|
148
133
|
if (entries.length === 0) {
|
|
149
134
|
return { content: 'No Vellum-provided skills available in the catalog.', isError: false };
|
|
150
135
|
}
|
|
@@ -157,52 +142,15 @@ class VellumSkillsCatalogTool implements Tool {
|
|
|
157
142
|
return { content: 'Error: skill_id is required for install action', isError: true };
|
|
158
143
|
}
|
|
159
144
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const skillFilePath = join(skillDir, 'SKILL.md');
|
|
163
|
-
|
|
164
|
-
if (!existsSync(skillFilePath)) {
|
|
165
|
-
const available = listCatalogEntries().map((e) => e.id);
|
|
166
|
-
return {
|
|
167
|
-
content: `Error: skill "${skillId}" not found in the Vellum catalog. Available: ${available.join(', ') || 'none'}`,
|
|
168
|
-
isError: true,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const content = readFileSync(skillFilePath, 'utf-8');
|
|
173
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
174
|
-
if (!match) {
|
|
175
|
-
return { content: `Error: skill "${skillId}" has invalid SKILL.md (missing frontmatter)`, isError: true };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const entry = parseCatalogEntry(skillDir);
|
|
179
|
-
if (!entry) {
|
|
180
|
-
return { content: `Error: skill "${skillId}" has invalid SKILL.md`, isError: true };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const bodyMarkdown = content.slice(match[0].length);
|
|
184
|
-
|
|
185
|
-
const result = createManagedSkill({
|
|
186
|
-
id: entry.id,
|
|
187
|
-
name: entry.name,
|
|
188
|
-
description: entry.description,
|
|
189
|
-
bodyMarkdown,
|
|
190
|
-
emoji: entry.emoji,
|
|
191
|
-
includes: entry.includes,
|
|
192
|
-
overwrite: input.overwrite === true,
|
|
193
|
-
addToIndex: true,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
if (!result.created) {
|
|
145
|
+
const result = await installFromVellumCatalog(skillId, { overwrite: input.overwrite === true });
|
|
146
|
+
if (!result.success) {
|
|
197
147
|
return { content: `Error: ${result.error}`, isError: true };
|
|
198
148
|
}
|
|
199
149
|
|
|
200
150
|
return {
|
|
201
151
|
content: JSON.stringify({
|
|
202
152
|
installed: true,
|
|
203
|
-
skill_id:
|
|
204
|
-
name: entry.name,
|
|
205
|
-
path: result.path,
|
|
153
|
+
skill_id: result.skillName,
|
|
206
154
|
}),
|
|
207
155
|
isError: false,
|
|
208
156
|
};
|
|
@@ -8,6 +8,7 @@ export async function executeSubagentSpawn(
|
|
|
8
8
|
const label = input.label as string;
|
|
9
9
|
const objective = input.objective as string;
|
|
10
10
|
const extraContext = input.context as string | undefined;
|
|
11
|
+
const sendResultToUser = input.send_result_to_user !== false;
|
|
11
12
|
|
|
12
13
|
if (!label || !objective) {
|
|
13
14
|
return { content: 'Both "label" and "objective" are required.', isError: true };
|
|
@@ -26,6 +27,7 @@ export async function executeSubagentSpawn(
|
|
|
26
27
|
label,
|
|
27
28
|
objective,
|
|
28
29
|
context: extraContext,
|
|
30
|
+
sendResultToUser,
|
|
29
31
|
},
|
|
30
32
|
sendToClient as (msg: unknown) => void,
|
|
31
33
|
);
|
|
@@ -36,7 +36,7 @@ export interface ParsedCommand {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const SHELL_PROGRAMS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'fish']);
|
|
39
|
-
const OPAQUE_PROGRAMS = new Set(['eval', 'source']);
|
|
39
|
+
const OPAQUE_PROGRAMS = new Set(['eval', 'source', 'alias']);
|
|
40
40
|
const DANGEROUS_ENV_VARS = new Set([
|
|
41
41
|
'LD_PRELOAD', 'LD_LIBRARY_PATH',
|
|
42
42
|
'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH', 'DYLD_FRAMEWORK_PATH',
|
|
@@ -89,17 +89,33 @@ const initGuard = new PromiseGuard<void>();
|
|
|
89
89
|
*/
|
|
90
90
|
function findWasmPath(pkg: string, file: string): string {
|
|
91
91
|
const dir = import.meta.dirname ?? __dirname;
|
|
92
|
-
const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
// In compiled Bun binaries, import.meta.dirname points into the virtual
|
|
94
|
+
// /$bunfs/ filesystem. Prefer bundled WASM assets shipped alongside the
|
|
95
|
+
// executable before falling back to process.cwd(), so we never accidentally
|
|
96
|
+
// pick up a mismatched version from the working directory.
|
|
97
|
+
if (dir.startsWith('/$bunfs/')) {
|
|
95
98
|
const execDir = dirname(process.execPath);
|
|
96
99
|
// macOS .app bundle: binary is in Contents/MacOS/, resources in Contents/Resources/
|
|
97
100
|
const resourcesPath = join(execDir, '..', 'Resources', file);
|
|
98
101
|
if (existsSync(resourcesPath)) return resourcesPath;
|
|
99
|
-
//
|
|
100
|
-
|
|
102
|
+
// Next to the binary itself (non-app-bundle deployments)
|
|
103
|
+
const execDirPath = join(execDir, file);
|
|
104
|
+
if (existsSync(execDirPath)) return execDirPath;
|
|
105
|
+
// Last resort: resolve from process.cwd() (the assistant/ directory)
|
|
106
|
+
const cwdPath = join(process.cwd(), 'node_modules', pkg, file);
|
|
107
|
+
if (existsSync(cwdPath)) return cwdPath;
|
|
108
|
+
return execDirPath;
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
|
|
112
|
+
|
|
113
|
+
if (existsSync(sourcePath)) return sourcePath;
|
|
114
|
+
|
|
115
|
+
// Fallback: resolve from process.cwd() (the assistant/ directory).
|
|
116
|
+
const cwdPath = join(process.cwd(), 'node_modules', pkg, file);
|
|
117
|
+
if (existsSync(cwdPath)) return cwdPath;
|
|
118
|
+
|
|
103
119
|
return sourcePath;
|
|
104
120
|
}
|
|
105
121
|
|
package/src/util/platform.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, existsSync, statSync, unlinkSync, renameSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, existsSync, statSync, unlinkSync, renameSync, readFileSync, writeFileSync, readdirSync, chmodSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
/**
|
|
@@ -565,6 +565,13 @@ export function ensureDataDir(): void {
|
|
|
565
565
|
mkdirSync(dir, { recursive: true });
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
|
+
// Lock down the root directory so only the owner can traverse it.
|
|
569
|
+
// Runtime files (socket, session token, PID) live directly under root.
|
|
570
|
+
try {
|
|
571
|
+
chmodSync(root, 0o700);
|
|
572
|
+
} catch {
|
|
573
|
+
// Non-fatal: some filesystems don't support Unix permissions
|
|
574
|
+
}
|
|
568
575
|
}
|
|
569
576
|
|
|
570
577
|
/**
|
package/src/util/retry.ts
CHANGED
|
@@ -58,10 +58,10 @@ export function getHttpRetryDelay(
|
|
|
58
58
|
const parsed = parseRetryAfterMs(retryAfter);
|
|
59
59
|
if (parsed !== undefined) return parsed;
|
|
60
60
|
}
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
return Math.max(baseDelayMs, computeRetryDelay(attempt,
|
|
61
|
+
// For attempt 0, double the base so jitter range [baseDelayMs, 2*baseDelayMs) stays above the floor.
|
|
62
|
+
// For attempt >= 1, use the original base — jitter is already above baseDelayMs.
|
|
63
|
+
const effectiveBase = attempt === 0 ? baseDelayMs * 2 : baseDelayMs;
|
|
64
|
+
return Math.max(baseDelayMs, computeRetryDelay(attempt, effectiveBase));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|