@link-assistant/agent 0.13.0 → 0.13.2
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/package.json +1 -1
- package/src/bun/index.ts +73 -7
- package/src/provider/models.ts +64 -1
package/package.json
CHANGED
package/src/bun/index.ts
CHANGED
|
@@ -13,26 +13,69 @@ export namespace BunProc {
|
|
|
13
13
|
// Lock key for serializing package installations to prevent race conditions
|
|
14
14
|
const INSTALL_LOCK_KEY = 'bun-install';
|
|
15
15
|
|
|
16
|
+
// Default timeout for subprocess commands (2 minutes)
|
|
17
|
+
// This prevents indefinite hangs from known Bun issues:
|
|
18
|
+
// - HTTP 304 response handling (https://github.com/oven-sh/bun/issues/5831)
|
|
19
|
+
// - Failed dependency fetch (https://github.com/oven-sh/bun/issues/26341)
|
|
20
|
+
// - IPv6 configuration issues
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
22
|
+
|
|
23
|
+
// Timeout specifically for package installation (60 seconds)
|
|
24
|
+
// Package installations should complete within this time for typical packages
|
|
25
|
+
const INSTALL_TIMEOUT_MS = 60000;
|
|
26
|
+
|
|
27
|
+
export const TimeoutError = NamedError.create(
|
|
28
|
+
'BunTimeoutError',
|
|
29
|
+
z.object({
|
|
30
|
+
cmd: z.array(z.string()),
|
|
31
|
+
timeoutMs: z.number(),
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
16
35
|
export async function run(
|
|
17
36
|
cmd: string[],
|
|
18
|
-
options?: Bun.SpawnOptions.OptionsObject<any, any, any>
|
|
37
|
+
options?: Bun.SpawnOptions.OptionsObject<any, any, any> & {
|
|
38
|
+
timeout?: number;
|
|
39
|
+
}
|
|
19
40
|
) {
|
|
41
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
42
|
+
|
|
20
43
|
log.info(() => ({
|
|
21
44
|
message: 'running',
|
|
22
45
|
cmd: [which(), ...cmd],
|
|
23
|
-
|
|
46
|
+
timeout,
|
|
47
|
+
cwd: options?.cwd,
|
|
24
48
|
}));
|
|
49
|
+
|
|
25
50
|
const result = Bun.spawn([which(), ...cmd], {
|
|
26
51
|
...options,
|
|
27
52
|
stdout: 'pipe',
|
|
28
53
|
stderr: 'pipe',
|
|
54
|
+
timeout, // Automatically kills process after timeout
|
|
55
|
+
killSignal: 'SIGTERM', // Graceful termination signal
|
|
29
56
|
env: {
|
|
30
57
|
...process.env,
|
|
31
58
|
...options?.env,
|
|
32
59
|
BUN_BE_BUN: '1',
|
|
33
60
|
},
|
|
34
61
|
});
|
|
62
|
+
|
|
35
63
|
const code = await result.exited;
|
|
64
|
+
|
|
65
|
+
// Check if process was killed due to timeout
|
|
66
|
+
if (result.signalCode === 'SIGTERM' && code !== 0) {
|
|
67
|
+
log.error(() => ({
|
|
68
|
+
message: 'command timed out',
|
|
69
|
+
cmd: [which(), ...cmd],
|
|
70
|
+
timeout,
|
|
71
|
+
signalCode: result.signalCode,
|
|
72
|
+
}));
|
|
73
|
+
throw new TimeoutError({
|
|
74
|
+
cmd: [which(), ...cmd],
|
|
75
|
+
timeoutMs: timeout,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
36
79
|
const stdout = result.stdout
|
|
37
80
|
? typeof result.stdout === 'number'
|
|
38
81
|
? result.stdout
|
|
@@ -84,6 +127,13 @@ export namespace BunProc {
|
|
|
84
127
|
);
|
|
85
128
|
}
|
|
86
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Check if an error is a timeout error
|
|
132
|
+
*/
|
|
133
|
+
function isTimeoutError(error: unknown): boolean {
|
|
134
|
+
return error instanceof TimeoutError;
|
|
135
|
+
}
|
|
136
|
+
|
|
87
137
|
/**
|
|
88
138
|
* Wait for a specified duration
|
|
89
139
|
*/
|
|
@@ -139,12 +189,13 @@ export namespace BunProc {
|
|
|
139
189
|
version,
|
|
140
190
|
}));
|
|
141
191
|
|
|
142
|
-
// Retry logic for cache-related errors
|
|
192
|
+
// Retry logic for cache-related errors and timeout errors
|
|
143
193
|
let lastError: Error | undefined;
|
|
144
194
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
145
195
|
try {
|
|
146
196
|
await BunProc.run(args, {
|
|
147
197
|
cwd: Global.Path.cache,
|
|
198
|
+
timeout: INSTALL_TIMEOUT_MS, // Use specific timeout for package installation
|
|
148
199
|
});
|
|
149
200
|
|
|
150
201
|
log.info(() => ({
|
|
@@ -159,6 +210,7 @@ export namespace BunProc {
|
|
|
159
210
|
} catch (e) {
|
|
160
211
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
161
212
|
const isCacheError = isCacheRelatedError(errorMsg);
|
|
213
|
+
const isTimeout = isTimeoutError(e);
|
|
162
214
|
|
|
163
215
|
log.warn(() => ({
|
|
164
216
|
message: 'package installation attempt failed',
|
|
@@ -168,11 +220,15 @@ export namespace BunProc {
|
|
|
168
220
|
maxRetries: MAX_RETRIES,
|
|
169
221
|
error: errorMsg,
|
|
170
222
|
isCacheError,
|
|
223
|
+
isTimeout,
|
|
171
224
|
}));
|
|
172
225
|
|
|
173
|
-
|
|
226
|
+
// Retry on cache-related errors or timeout errors
|
|
227
|
+
if ((isCacheError || isTimeout) && attempt < MAX_RETRIES) {
|
|
174
228
|
log.info(() => ({
|
|
175
|
-
message:
|
|
229
|
+
message: isTimeout
|
|
230
|
+
? 'retrying installation after timeout (possible network issue)'
|
|
231
|
+
: 'retrying installation after cache-related error',
|
|
176
232
|
pkg,
|
|
177
233
|
version,
|
|
178
234
|
attempt,
|
|
@@ -184,7 +240,7 @@ export namespace BunProc {
|
|
|
184
240
|
continue;
|
|
185
241
|
}
|
|
186
242
|
|
|
187
|
-
// Non-
|
|
243
|
+
// Non-retriable error or final attempt - log and throw
|
|
188
244
|
log.error(() => ({
|
|
189
245
|
message: 'package installation failed',
|
|
190
246
|
pkg,
|
|
@@ -192,10 +248,11 @@ export namespace BunProc {
|
|
|
192
248
|
error: errorMsg,
|
|
193
249
|
stack: e instanceof Error ? e.stack : undefined,
|
|
194
250
|
possibleCacheCorruption: isCacheError,
|
|
251
|
+
timedOut: isTimeout,
|
|
195
252
|
attempts: attempt,
|
|
196
253
|
}));
|
|
197
254
|
|
|
198
|
-
// Provide helpful recovery instructions
|
|
255
|
+
// Provide helpful recovery instructions
|
|
199
256
|
if (isCacheError) {
|
|
200
257
|
log.error(() => ({
|
|
201
258
|
message:
|
|
@@ -203,6 +260,15 @@ export namespace BunProc {
|
|
|
203
260
|
}));
|
|
204
261
|
}
|
|
205
262
|
|
|
263
|
+
if (isTimeout) {
|
|
264
|
+
log.error(() => ({
|
|
265
|
+
message:
|
|
266
|
+
'Package installation timed out. This may be due to network issues or Bun hanging. ' +
|
|
267
|
+
'Try: 1) Check network connectivity, 2) Run "bun pm cache rm" to clear cache, ' +
|
|
268
|
+
'3) Check for IPv6 issues (try disabling IPv6)',
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
206
272
|
throw new InstallFailedError(
|
|
207
273
|
{ pkg, version, details: errorMsg },
|
|
208
274
|
{
|
package/src/provider/models.ts
CHANGED
|
@@ -67,11 +67,74 @@ export namespace ModelsDev {
|
|
|
67
67
|
|
|
68
68
|
export type Provider = z.infer<typeof Provider>;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Cache staleness threshold in milliseconds (1 hour).
|
|
72
|
+
* If the cache is older than this, we await the refresh before using the data.
|
|
73
|
+
*/
|
|
74
|
+
const CACHE_STALE_THRESHOLD_MS = 60 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the models database, refreshing from models.dev if needed.
|
|
78
|
+
*
|
|
79
|
+
* This function handles cache staleness properly:
|
|
80
|
+
* - If cache doesn't exist: await refresh to ensure fresh data
|
|
81
|
+
* - If cache is stale (> 1 hour old): await refresh to ensure up-to-date models
|
|
82
|
+
* - If cache is fresh: trigger background refresh but use cached data immediately
|
|
83
|
+
*
|
|
84
|
+
* This prevents ProviderModelNotFoundError when:
|
|
85
|
+
* - User runs agent for the first time (no cache)
|
|
86
|
+
* - User has outdated cache missing new models like kimi-k2.5-free
|
|
87
|
+
*
|
|
88
|
+
* @see https://github.com/link-assistant/agent/issues/175
|
|
89
|
+
*/
|
|
70
90
|
export async function get() {
|
|
71
|
-
refresh();
|
|
72
91
|
const file = Bun.file(filepath);
|
|
92
|
+
|
|
93
|
+
// Check if cache exists and get its modification time
|
|
94
|
+
const exists = await file.exists();
|
|
95
|
+
|
|
96
|
+
if (!exists) {
|
|
97
|
+
// No cache - must await refresh to get initial data
|
|
98
|
+
log.info(() => ({
|
|
99
|
+
message: 'no cache found, awaiting refresh',
|
|
100
|
+
path: filepath,
|
|
101
|
+
}));
|
|
102
|
+
await refresh();
|
|
103
|
+
} else {
|
|
104
|
+
// Check if cache is stale
|
|
105
|
+
const stats = await file.stat().catch(() => null);
|
|
106
|
+
const mtime = stats?.mtime?.getTime() ?? 0;
|
|
107
|
+
const isStale = Date.now() - mtime > CACHE_STALE_THRESHOLD_MS;
|
|
108
|
+
|
|
109
|
+
if (isStale) {
|
|
110
|
+
// Stale cache - await refresh to get updated model list
|
|
111
|
+
log.info(() => ({
|
|
112
|
+
message: 'cache is stale, awaiting refresh',
|
|
113
|
+
path: filepath,
|
|
114
|
+
age: Date.now() - mtime,
|
|
115
|
+
threshold: CACHE_STALE_THRESHOLD_MS,
|
|
116
|
+
}));
|
|
117
|
+
await refresh();
|
|
118
|
+
} else {
|
|
119
|
+
// Fresh cache - trigger background refresh but don't wait
|
|
120
|
+
log.info(() => ({
|
|
121
|
+
message: 'cache is fresh, triggering background refresh',
|
|
122
|
+
path: filepath,
|
|
123
|
+
age: Date.now() - mtime,
|
|
124
|
+
}));
|
|
125
|
+
refresh();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Now read the cache file
|
|
73
130
|
const result = await file.json().catch(() => {});
|
|
74
131
|
if (result) return result as Record<string, Provider>;
|
|
132
|
+
|
|
133
|
+
// Fallback to bundled data if cache read failed
|
|
134
|
+
log.warn(() => ({
|
|
135
|
+
message: 'cache read failed, using bundled data',
|
|
136
|
+
path: filepath,
|
|
137
|
+
}));
|
|
75
138
|
const json = await data();
|
|
76
139
|
return JSON.parse(json) as Record<string, Provider>;
|
|
77
140
|
}
|