talon-agent 1.0.0 → 1.2.0
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/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +15 -11
- package/prompts/dream.md +7 -3
- package/prompts/heartbeat.md +30 -0
- package/prompts/identity.md +1 -0
- package/prompts/teams.md +3 -0
- package/prompts/telegram.md +1 -0
- package/src/__tests__/chat-settings.test.ts +108 -2
- package/src/__tests__/cleanup-registry.test.ts +58 -0
- package/src/__tests__/config.test.ts +118 -52
- package/src/__tests__/cron-store-extended.test.ts +661 -0
- package/src/__tests__/cron-store.test.ts +145 -11
- package/src/__tests__/daily-log.test.ts +224 -13
- package/src/__tests__/dispatcher.test.ts +424 -23
- package/src/__tests__/dream.test.ts +1028 -0
- package/src/__tests__/errors-extended.test.ts +428 -0
- package/src/__tests__/errors.test.ts +95 -3
- package/src/__tests__/fuzz.test.ts +87 -15
- package/src/__tests__/gateway-actions.test.ts +1174 -433
- package/src/__tests__/gateway-http.test.ts +210 -19
- package/src/__tests__/gateway-retry.test.ts +359 -0
- package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
- package/src/__tests__/graph.test.ts +830 -0
- package/src/__tests__/handlers-stream.test.ts +208 -0
- package/src/__tests__/handlers.test.ts +2539 -70
- package/src/__tests__/heartbeat.test.ts +364 -0
- package/src/__tests__/history-extended.test.ts +775 -0
- package/src/__tests__/history-persistence.test.ts +74 -19
- package/src/__tests__/history.test.ts +113 -79
- package/src/__tests__/integration.test.ts +43 -8
- package/src/__tests__/log-init.test.ts +129 -0
- package/src/__tests__/log.test.ts +23 -5
- package/src/__tests__/media-index.test.ts +317 -35
- package/src/__tests__/plugin.test.ts +314 -0
- package/src/__tests__/prompt-builder-extended.test.ts +296 -0
- package/src/__tests__/prompt-builder.test.ts +44 -9
- package/src/__tests__/sessions.test.ts +258 -4
- package/src/__tests__/storage-save-errors.test.ts +342 -0
- package/src/__tests__/teams-frontend.test.ts +526 -31
- package/src/__tests__/telegram-formatting.test.ts +82 -0
- package/src/__tests__/terminal-commands.test.ts +208 -1
- package/src/__tests__/terminal-renderer.test.ts +223 -0
- package/src/__tests__/time.test.ts +107 -0
- package/src/__tests__/workspace-migrate.test.ts +256 -0
- package/src/__tests__/workspace.test.ts +63 -1
- package/src/backend/claude-sdk/tools.ts +64 -18
- package/src/bootstrap.ts +14 -14
- package/src/cli.ts +440 -125
- package/src/core/cron.ts +20 -5
- package/src/core/dispatcher.ts +27 -9
- package/src/core/dream.ts +79 -24
- package/src/core/errors.ts +12 -2
- package/src/core/gateway-actions.ts +182 -46
- package/src/core/gateway.ts +93 -41
- package/src/core/heartbeat.ts +515 -0
- package/src/core/plugin.ts +1 -1
- package/src/core/prompt-builder.ts +1 -4
- package/src/core/pulse.ts +4 -3
- package/src/frontend/teams/actions.ts +3 -1
- package/src/frontend/teams/formatting.ts +47 -8
- package/src/frontend/teams/graph.ts +35 -11
- package/src/frontend/teams/index.ts +155 -57
- package/src/frontend/teams/tools.ts +4 -6
- package/src/frontend/telegram/actions.ts +358 -82
- package/src/frontend/telegram/admin.ts +162 -72
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +37 -21
- package/src/frontend/telegram/formatting.ts +2 -4
- package/src/frontend/telegram/handlers.ts +262 -66
- package/src/frontend/telegram/index.ts +39 -14
- package/src/frontend/telegram/middleware.ts +14 -4
- package/src/frontend/telegram/userbot.ts +16 -4
- package/src/frontend/terminal/renderer.ts +1 -4
- package/src/index.ts +28 -4
- package/src/storage/chat-settings.ts +32 -9
- package/src/storage/cron-store.ts +53 -11
- package/src/storage/daily-log.ts +72 -19
- package/src/storage/history.ts +39 -21
- package/src/storage/media-index.ts +37 -12
- package/src/storage/sessions.ts +3 -2
- package/src/util/cleanup-registry.ts +34 -0
- package/src/util/config.ts +85 -23
- package/src/util/log.ts +47 -17
- package/src/util/paths.ts +10 -0
- package/src/util/time.ts +29 -6
- package/src/util/watchdog.ts +5 -1
- package/src/util/workspace.ts +51 -10
package/src/core/cron.ts
CHANGED
|
@@ -65,10 +65,16 @@ async function runCronTick(): Promise<void> {
|
|
|
65
65
|
if (getActiveCount() > 10) break;
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
|
-
log(
|
|
68
|
+
log(
|
|
69
|
+
"cron",
|
|
70
|
+
`Executing "${job.name}" [${job.id}] (${job.type}) in chat ${job.chatId}`,
|
|
71
|
+
);
|
|
69
72
|
await executeJob(job);
|
|
70
73
|
recordCronRun(job.id);
|
|
71
|
-
appendDailyLog(
|
|
74
|
+
appendDailyLog(
|
|
75
|
+
"Cron",
|
|
76
|
+
`Ran "${job.name}" (${job.type}) in chat ${job.chatId}`,
|
|
77
|
+
);
|
|
72
78
|
log("cron", `Executed "${job.name}" [${job.id}] in chat ${job.chatId}`);
|
|
73
79
|
} catch (err) {
|
|
74
80
|
logError("cron", `Job "${job.name}" [${job.id}] failed`, err);
|
|
@@ -79,7 +85,9 @@ async function runCronTick(): Promise<void> {
|
|
|
79
85
|
function isDue(job: CronJob, now: Date): boolean {
|
|
80
86
|
try {
|
|
81
87
|
const oneMinuteAgo = new Date(now.getTime() - 60_000);
|
|
82
|
-
const cron = new Cron(job.schedule, {
|
|
88
|
+
const cron = new Cron(job.schedule, {
|
|
89
|
+
timezone: job.timezone ?? undefined,
|
|
90
|
+
});
|
|
83
91
|
const next = cron.nextRun(oneMinuteAgo);
|
|
84
92
|
if (!next) return false;
|
|
85
93
|
|
|
@@ -102,10 +110,17 @@ function isDue(job: CronJob, now: Date): boolean {
|
|
|
102
110
|
|
|
103
111
|
const CRON_JOB_TIMEOUT_MS = 10 * 60_000; // 10-minute max per job
|
|
104
112
|
|
|
105
|
-
async function withTimeout<T>(
|
|
113
|
+
async function withTimeout<T>(
|
|
114
|
+
promise: Promise<T>,
|
|
115
|
+
ms: number,
|
|
116
|
+
label: string,
|
|
117
|
+
): Promise<T> {
|
|
106
118
|
let timer: ReturnType<typeof setTimeout>;
|
|
107
119
|
const timeout = new Promise<never>((_, reject) => {
|
|
108
|
-
timer = setTimeout(
|
|
120
|
+
timer = setTimeout(
|
|
121
|
+
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
|
122
|
+
ms,
|
|
123
|
+
);
|
|
109
124
|
});
|
|
110
125
|
try {
|
|
111
126
|
return await Promise.race([promise, timeout]);
|
package/src/core/dispatcher.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
ExecuteParams,
|
|
17
17
|
ExecuteResult,
|
|
18
18
|
} from "./types.js";
|
|
19
|
-
import { log, logDebug } from "../util/log.js";
|
|
19
|
+
import { log, logDebug, logWarn } from "../util/log.js";
|
|
20
20
|
import { maybeStartDream } from "./dream.js";
|
|
21
21
|
|
|
22
22
|
// ── Dependencies (injected at startup) ──────────────────────────────────────
|
|
@@ -67,9 +67,11 @@ export async function execute(params: ExecuteParams): Promise<ExecuteResult> {
|
|
|
67
67
|
chatChains.set(chatId, queued); // must happen before any await
|
|
68
68
|
|
|
69
69
|
// Clean up chain entry when this is the last in the chain
|
|
70
|
-
queued
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
queued
|
|
71
|
+
.catch(() => {})
|
|
72
|
+
.finally(() => {
|
|
73
|
+
if (chatChains.get(chatId) === queued) chatChains.delete(chatId);
|
|
74
|
+
});
|
|
73
75
|
|
|
74
76
|
return queued;
|
|
75
77
|
}
|
|
@@ -90,14 +92,27 @@ async function executeInner(params: ExecuteParams): Promise<ExecuteResult> {
|
|
|
90
92
|
// Dream check — fire-and-forget background memory consolidation if due
|
|
91
93
|
maybeStartDream();
|
|
92
94
|
|
|
93
|
-
logDebug(
|
|
95
|
+
logDebug(
|
|
96
|
+
"dispatcher",
|
|
97
|
+
`[${reqId}] ${params.source} chat=${params.chatId} started (active=${activeCount})`,
|
|
98
|
+
);
|
|
94
99
|
context.acquire(params.numericChatId, params.chatId);
|
|
95
100
|
|
|
96
101
|
let typingTimer: ReturnType<typeof setInterval> | undefined;
|
|
97
102
|
try {
|
|
98
|
-
await sendTyping(params.numericChatId).catch(() => {
|
|
103
|
+
await sendTyping(params.numericChatId).catch((err: unknown) => {
|
|
104
|
+
logWarn(
|
|
105
|
+
"dispatcher",
|
|
106
|
+
`sendTyping failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
99
109
|
typingTimer = setInterval(() => {
|
|
100
|
-
sendTyping(params.numericChatId).catch(() => {
|
|
110
|
+
sendTyping(params.numericChatId).catch((err: unknown) => {
|
|
111
|
+
logWarn(
|
|
112
|
+
"dispatcher",
|
|
113
|
+
`sendTyping interval failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
101
116
|
}, 4000);
|
|
102
117
|
|
|
103
118
|
const result = await backend.query({
|
|
@@ -113,14 +128,17 @@ async function executeInner(params: ExecuteParams): Promise<ExecuteResult> {
|
|
|
113
128
|
|
|
114
129
|
onActivity();
|
|
115
130
|
|
|
116
|
-
logDebug(
|
|
131
|
+
logDebug(
|
|
132
|
+
"dispatcher",
|
|
133
|
+
`[${reqId}] completed in ${result.durationMs}ms (in=${result.inputTokens} out=${result.outputTokens})`,
|
|
134
|
+
);
|
|
117
135
|
|
|
118
136
|
return {
|
|
119
137
|
...result,
|
|
120
138
|
bridgeMessageCount: context.getMessageCount(params.numericChatId),
|
|
121
139
|
};
|
|
122
140
|
} finally {
|
|
123
|
-
|
|
141
|
+
clearInterval(typingTimer);
|
|
124
142
|
context.release(params.numericChatId);
|
|
125
143
|
}
|
|
126
144
|
}
|
package/src/core/dream.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { log, logError, logWarn } from "../util/log.js";
|
|
|
25
25
|
export type DreamState = {
|
|
26
26
|
/** Unix millisecond timestamp of the last completed dream run. */
|
|
27
27
|
last_run: number;
|
|
28
|
+
/** Human-readable ISO timestamp of the last completed dream run. */
|
|
29
|
+
last_run_at?: string;
|
|
28
30
|
/** "idle" when no dream is running, "running" while one is active. */
|
|
29
31
|
status: "idle" | "running";
|
|
30
32
|
};
|
|
@@ -39,7 +41,12 @@ const DREAM_LOGS_DIR = resolve(dirs.logs, "dreams");
|
|
|
39
41
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
40
42
|
|
|
41
43
|
let dreaming = false; // in-process guard (one dream at a time)
|
|
42
|
-
let configRef: {
|
|
44
|
+
let configRef: {
|
|
45
|
+
model?: string;
|
|
46
|
+
dreamModel?: string;
|
|
47
|
+
claudeBinary?: string;
|
|
48
|
+
workspace?: string;
|
|
49
|
+
} | null = null;
|
|
43
50
|
|
|
44
51
|
export function initDream(cfg: {
|
|
45
52
|
model?: string;
|
|
@@ -86,12 +93,18 @@ async function executeDream(trigger: "auto" | "forced"): Promise<void> {
|
|
|
86
93
|
|
|
87
94
|
dreaming = true;
|
|
88
95
|
writeDreamState({ last_run: now, status: "running" });
|
|
89
|
-
log(
|
|
96
|
+
log(
|
|
97
|
+
"dream",
|
|
98
|
+
`${trigger === "forced" ? "Force-triggering" : "Triggering"} memory consolidation (last run: ${state?.last_run ? new Date(state.last_run).toISOString() : "never"})`,
|
|
99
|
+
);
|
|
90
100
|
|
|
91
101
|
try {
|
|
92
102
|
const dreamLogPath = await runDreamAgent(state?.last_run ?? 0);
|
|
93
103
|
writeDreamState({ last_run: Date.now(), status: "idle" });
|
|
94
|
-
log(
|
|
104
|
+
log(
|
|
105
|
+
"dream",
|
|
106
|
+
`Memory consolidation complete (${trigger}), log: ${dreamLogPath}`,
|
|
107
|
+
);
|
|
95
108
|
} catch (err) {
|
|
96
109
|
logError("dream", `Memory consolidation failed (${trigger})`, err);
|
|
97
110
|
writeDreamState({ last_run: Date.now(), status: "idle" });
|
|
@@ -109,9 +122,10 @@ async function runDreamAgent(lastRunTimestamp: number): Promise<string> {
|
|
|
109
122
|
return "";
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
const lastRunIso =
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
const lastRunIso =
|
|
126
|
+
lastRunTimestamp > 0
|
|
127
|
+
? new Date(lastRunTimestamp).toISOString()
|
|
128
|
+
: "the beginning of time";
|
|
115
129
|
|
|
116
130
|
const logsDir = dirs.logs;
|
|
117
131
|
const memoryFile = pathFiles.memory;
|
|
@@ -127,7 +141,8 @@ async function runDreamAgent(lastRunTimestamp: number): Promise<string> {
|
|
|
127
141
|
.replace(/\{\{dreamStateFile\}\}/g, dreamStateFile)
|
|
128
142
|
.replace(/\{\{logsDir\}\}/g, logsDir)
|
|
129
143
|
.replace(/\{\{lastRunIso\}\}/g, lastRunIso)
|
|
130
|
-
.replace(/\{\{memoryFile\}\}/g, memoryFile)
|
|
144
|
+
.replace(/\{\{memoryFile\}\}/g, memoryFile)
|
|
145
|
+
.replace(/\{\{dailyMemoryDir\}\}/g, dirs.dailyMemory);
|
|
131
146
|
} catch {
|
|
132
147
|
throw new Error(`Failed to read dream prompt from ${promptPath}`);
|
|
133
148
|
}
|
|
@@ -138,12 +153,19 @@ async function runDreamAgent(lastRunTimestamp: number): Promise<string> {
|
|
|
138
153
|
// Set up dream log file
|
|
139
154
|
const dreamLogFile = createDreamLogFile();
|
|
140
155
|
appendDreamLog(dreamLogFile, `# Dream Run — ${new Date().toISOString()}\n`);
|
|
141
|
-
appendDreamLog(
|
|
142
|
-
|
|
156
|
+
appendDreamLog(
|
|
157
|
+
dreamLogFile,
|
|
158
|
+
`**Trigger:** last_run=${lastRunIso}, model=${model}\n`,
|
|
159
|
+
);
|
|
160
|
+
appendDreamLog(
|
|
161
|
+
dreamLogFile,
|
|
162
|
+
`**Prompt:**\n\`\`\`\n${prompt}\n\`\`\`\n\n---\n`,
|
|
163
|
+
);
|
|
143
164
|
|
|
144
165
|
const options = {
|
|
145
166
|
model,
|
|
146
|
-
systemPrompt:
|
|
167
|
+
systemPrompt:
|
|
168
|
+
"You are a background memory consolidation agent for Talon. Use only filesystem tools. Be precise and surgical — update memory.md without losing existing accurate information.",
|
|
147
169
|
cwd: workspace,
|
|
148
170
|
permissionMode: "bypassPermissions" as const,
|
|
149
171
|
allowDangerouslySkipPermissions: true,
|
|
@@ -171,7 +193,10 @@ async function runDreamAgent(lastRunTimestamp: number): Promise<string> {
|
|
|
171
193
|
};
|
|
172
194
|
|
|
173
195
|
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
174
|
-
setTimeout(
|
|
196
|
+
setTimeout(
|
|
197
|
+
() => reject(new Error("Dream agent timed out")),
|
|
198
|
+
DREAM_TIMEOUT_MS,
|
|
199
|
+
),
|
|
175
200
|
);
|
|
176
201
|
|
|
177
202
|
const agentPromise = (async () => {
|
|
@@ -182,13 +207,19 @@ async function runDreamAgent(lastRunTimestamp: number): Promise<string> {
|
|
|
182
207
|
for await (const msg of qi) {
|
|
183
208
|
logDreamMessage(dreamLogFile, msg);
|
|
184
209
|
}
|
|
185
|
-
appendDreamLog(
|
|
210
|
+
appendDreamLog(
|
|
211
|
+
dreamLogFile,
|
|
212
|
+
`\n---\n**Dream completed at ${new Date().toISOString()}**\n`,
|
|
213
|
+
);
|
|
186
214
|
})();
|
|
187
215
|
|
|
188
216
|
try {
|
|
189
217
|
await Promise.race([agentPromise, timeoutPromise]);
|
|
190
218
|
} catch (err) {
|
|
191
|
-
appendDreamLog(
|
|
219
|
+
appendDreamLog(
|
|
220
|
+
dreamLogFile,
|
|
221
|
+
`\n---\n**Dream FAILED at ${new Date().toISOString()}:** ${err}\n`,
|
|
222
|
+
);
|
|
192
223
|
throw err;
|
|
193
224
|
}
|
|
194
225
|
|
|
@@ -223,7 +254,7 @@ function logDreamMessage(logFile: string, msg: SDKMessage): void {
|
|
|
223
254
|
// Extract text content from the assistant message
|
|
224
255
|
const textBlocks = msg.message.content
|
|
225
256
|
.filter((b) => b.type === "text")
|
|
226
|
-
.map((b) => "text" in b ? (b as { text: string }).text : "");
|
|
257
|
+
.map((b) => ("text" in b ? (b as { text: string }).text : ""));
|
|
227
258
|
const toolUseBlocks = msg.message.content
|
|
228
259
|
.filter((b) => b.type === "tool_use")
|
|
229
260
|
.map((b) => {
|
|
@@ -232,7 +263,10 @@ function logDreamMessage(logFile: string, msg: SDKMessage): void {
|
|
|
232
263
|
});
|
|
233
264
|
|
|
234
265
|
if (textBlocks.length > 0) {
|
|
235
|
-
appendDreamLog(
|
|
266
|
+
appendDreamLog(
|
|
267
|
+
logFile,
|
|
268
|
+
`\n## [${ts}] Assistant\n${textBlocks.join("\n")}\n`,
|
|
269
|
+
);
|
|
236
270
|
}
|
|
237
271
|
if (toolUseBlocks.length > 0) {
|
|
238
272
|
appendDreamLog(logFile, `\n${toolUseBlocks.join("\n\n")}\n`);
|
|
@@ -241,9 +275,18 @@ function logDreamMessage(logFile: string, msg: SDKMessage): void {
|
|
|
241
275
|
}
|
|
242
276
|
case "result": {
|
|
243
277
|
// Final result of the dream agent run
|
|
244
|
-
const result =
|
|
245
|
-
|
|
246
|
-
|
|
278
|
+
const result =
|
|
279
|
+
"result" in msg
|
|
280
|
+
? (msg as { result: string }).result
|
|
281
|
+
: JSON.stringify(msg);
|
|
282
|
+
const truncated =
|
|
283
|
+
result.length > 2000
|
|
284
|
+
? result.slice(0, 2000) + "\n... (truncated)"
|
|
285
|
+
: result;
|
|
286
|
+
appendDreamLog(
|
|
287
|
+
logFile,
|
|
288
|
+
`\n### [${ts}] Result (${msg.subtype})\n\`\`\`\n${truncated}\n\`\`\`\n`,
|
|
289
|
+
);
|
|
247
290
|
break;
|
|
248
291
|
}
|
|
249
292
|
case "system": {
|
|
@@ -253,11 +296,16 @@ function logDreamMessage(logFile: string, msg: SDKMessage): void {
|
|
|
253
296
|
case "user": {
|
|
254
297
|
// Tool results come back as user messages
|
|
255
298
|
if (msg.tool_use_result != null) {
|
|
256
|
-
const raw =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
299
|
+
const raw =
|
|
300
|
+
typeof msg.tool_use_result === "string"
|
|
301
|
+
? msg.tool_use_result
|
|
302
|
+
: JSON.stringify(msg.tool_use_result, null, 2);
|
|
303
|
+
const truncated =
|
|
304
|
+
raw.length > 2000 ? raw.slice(0, 2000) + "\n... (truncated)" : raw;
|
|
305
|
+
appendDreamLog(
|
|
306
|
+
logFile,
|
|
307
|
+
`\n### [${ts}] Tool Result\n\`\`\`\n${truncated}\n\`\`\`\n`,
|
|
308
|
+
);
|
|
261
309
|
}
|
|
262
310
|
break;
|
|
263
311
|
}
|
|
@@ -288,7 +336,14 @@ function writeDreamState(state: DreamState): void {
|
|
|
288
336
|
try {
|
|
289
337
|
const dir = resolve(DREAM_STATE_FILE, "..");
|
|
290
338
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
291
|
-
|
|
339
|
+
const enriched: DreamState = {
|
|
340
|
+
...state,
|
|
341
|
+
last_run_at: new Date(state.last_run).toISOString(),
|
|
342
|
+
};
|
|
343
|
+
writeFileAtomic.sync(
|
|
344
|
+
DREAM_STATE_FILE,
|
|
345
|
+
JSON.stringify(enriched, null, 2) + "\n",
|
|
346
|
+
);
|
|
292
347
|
} catch (err) {
|
|
293
348
|
logError("dream", "Failed to write dream state", err);
|
|
294
349
|
}
|
package/src/core/errors.ts
CHANGED
|
@@ -59,7 +59,13 @@ export function classify(err: unknown): TalonError {
|
|
|
59
59
|
let msg: string;
|
|
60
60
|
if (err instanceof Error) msg = err.message;
|
|
61
61
|
else if (typeof err === "string") msg = err;
|
|
62
|
-
else {
|
|
62
|
+
else {
|
|
63
|
+
try {
|
|
64
|
+
msg = String(err);
|
|
65
|
+
} catch {
|
|
66
|
+
msg = "[non-stringifiable error]";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
63
69
|
const cause = err instanceof Error ? err : undefined;
|
|
64
70
|
|
|
65
71
|
// Extract HTTP status if present
|
|
@@ -93,7 +99,11 @@ export function classify(err: unknown): TalonError {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
// Network errors
|
|
96
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
/network|ECONNREFUSED|ECONNRESET|ECONNABORTED|ETIMEDOUT|ENOTFOUND|fetch failed|connection reset/i.test(
|
|
104
|
+
msg,
|
|
105
|
+
)
|
|
106
|
+
) {
|
|
97
107
|
return new TalonError(msg, {
|
|
98
108
|
reason: "network",
|
|
99
109
|
retryable: true,
|