@link-assistant/agent 0.3.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/bun/index.ts CHANGED
@@ -5,10 +5,14 @@ import path from 'path';
5
5
  import { NamedError } from '../util/error';
6
6
  import { readableStreamToText } from 'bun';
7
7
  import { Flag } from '../flag/flag';
8
+ import { Lock } from '../util/lock';
8
9
 
9
10
  export namespace BunProc {
10
11
  const log = Log.create({ service: 'bun' });
11
12
 
13
+ // Lock key for serializing package installations to prevent race conditions
14
+ const INSTALL_LOCK_KEY = 'bun-install';
15
+
12
16
  export async function run(
13
17
  cmd: string[],
14
18
  options?: Bun.SpawnOptions.OptionsObject<any, any, any>
@@ -65,8 +69,38 @@ export namespace BunProc {
65
69
  })
66
70
  );
67
71
 
72
+ // Maximum number of retry attempts for cache-related errors
73
+ const MAX_RETRIES = 3;
74
+ // Delay between retries in milliseconds
75
+ const RETRY_DELAY_MS = 500;
76
+
77
+ /**
78
+ * Check if an error is related to Bun cache issues
79
+ */
80
+ function isCacheRelatedError(errorMsg: string): boolean {
81
+ return (
82
+ errorMsg.includes('failed copying files from cache') ||
83
+ errorMsg.includes('FileNotFound') ||
84
+ errorMsg.includes('ENOENT') ||
85
+ errorMsg.includes('EACCES') ||
86
+ errorMsg.includes('EBUSY')
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Wait for a specified duration
92
+ */
93
+ function delay(ms: number): Promise<void> {
94
+ return new Promise((resolve) => setTimeout(resolve, ms));
95
+ }
96
+
68
97
  export async function install(pkg: string, version = 'latest') {
69
98
  const mod = path.join(Global.Path.cache, 'node_modules', pkg);
99
+
100
+ // Use a write lock to serialize all package installations
101
+ // This prevents race conditions when multiple packages are installed concurrently
102
+ using _ = await Lock.write(INSTALL_LOCK_KEY);
103
+
70
104
  const pkgjson = Bun.file(path.join(Global.Path.cache, 'package.json'));
71
105
  const parsed = await pkgjson.json().catch(async () => {
72
106
  const result = { dependencies: {} };
@@ -108,25 +142,78 @@ export namespace BunProc {
108
142
  version,
109
143
  });
110
144
 
111
- await BunProc.run(args, {
112
- cwd: Global.Path.cache,
113
- }).catch((e) => {
114
- log.error('package installation failed', {
145
+ // Retry logic for cache-related errors
146
+ let lastError: Error | undefined;
147
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
148
+ try {
149
+ await BunProc.run(args, {
150
+ cwd: Global.Path.cache,
151
+ });
152
+
153
+ log.info('package installed successfully', { pkg, version, attempt });
154
+ parsed.dependencies[pkg] = version;
155
+ await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
156
+ return mod;
157
+ } catch (e) {
158
+ const errorMsg = e instanceof Error ? e.message : String(e);
159
+ const isCacheError = isCacheRelatedError(errorMsg);
160
+
161
+ log.warn('package installation attempt failed', {
162
+ pkg,
163
+ version,
164
+ attempt,
165
+ maxRetries: MAX_RETRIES,
166
+ error: errorMsg,
167
+ isCacheError,
168
+ });
169
+
170
+ if (isCacheError && attempt < MAX_RETRIES) {
171
+ log.info('retrying installation after cache-related error', {
172
+ pkg,
173
+ version,
174
+ attempt,
175
+ nextAttempt: attempt + 1,
176
+ delayMs: RETRY_DELAY_MS,
177
+ });
178
+ await delay(RETRY_DELAY_MS);
179
+ lastError = e instanceof Error ? e : new Error(errorMsg);
180
+ continue;
181
+ }
182
+
183
+ // Non-cache error or final attempt - log and throw
184
+ log.error('package installation failed', {
185
+ pkg,
186
+ version,
187
+ error: errorMsg,
188
+ stack: e instanceof Error ? e.stack : undefined,
189
+ possibleCacheCorruption: isCacheError,
190
+ attempts: attempt,
191
+ });
192
+
193
+ // Provide helpful recovery instructions for cache-related errors
194
+ if (isCacheError) {
195
+ log.error(
196
+ 'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm'
197
+ );
198
+ }
199
+
200
+ throw new InstallFailedError(
201
+ { pkg, version, details: errorMsg },
202
+ {
203
+ cause: e,
204
+ }
205
+ );
206
+ }
207
+ }
208
+
209
+ // This should not be reached, but handle it just in case
210
+ throw new InstallFailedError(
211
+ {
115
212
  pkg,
116
213
  version,
117
- error: e instanceof Error ? e.message : String(e),
118
- stack: e instanceof Error ? e.stack : undefined,
119
- });
120
- throw new InstallFailedError(
121
- { pkg, version, details: e instanceof Error ? e.message : String(e) },
122
- {
123
- cause: e,
124
- }
125
- );
126
- });
127
- log.info('package installed successfully', { pkg, version });
128
- parsed.dependencies[pkg] = version;
129
- await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
130
- return mod;
214
+ details: lastError?.message ?? 'Installation failed after all retries',
215
+ },
216
+ { cause: lastError }
217
+ );
131
218
  }
132
219
  }
@@ -905,6 +905,10 @@ export namespace Provider {
905
905
  if (opencodeProvider) {
906
906
  const [model] = sort(Object.values(opencodeProvider.info.models));
907
907
  if (model) {
908
+ log.info('using opencode provider as default', {
909
+ provider: opencodeProvider.info.id,
910
+ model: model.id,
911
+ });
908
912
  return {
909
913
  providerID: opencodeProvider.info.id,
910
914
  modelID: model.id,
@@ -912,7 +916,7 @@ export namespace Provider {
912
916
  }
913
917
  }
914
918
 
915
- // Fall back to any available provider
919
+ // Fall back to any available provider if opencode is not available
916
920
  const provider = providers.find(
917
921
  (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
918
922
  );
@@ -290,10 +290,27 @@ export namespace SessionPrompt {
290
290
  history: msgs,
291
291
  });
292
292
 
293
- const model = await Provider.getModel(
294
- lastUser.model.providerID,
295
- lastUser.model.modelID
296
- );
293
+ let model;
294
+ try {
295
+ model = await Provider.getModel(
296
+ lastUser.model.providerID,
297
+ lastUser.model.modelID
298
+ );
299
+ } catch (error) {
300
+ log.warn(
301
+ 'Failed to initialize specified model, falling back to default model',
302
+ {
303
+ providerID: lastUser.model.providerID,
304
+ modelID: lastUser.model.modelID,
305
+ error: error instanceof Error ? error.message : String(error),
306
+ }
307
+ );
308
+ const defaultModel = await Provider.defaultModel();
309
+ model = await Provider.getModel(
310
+ defaultModel.providerID,
311
+ defaultModel.modelID
312
+ );
313
+ }
297
314
  const task = tasks.pop();
298
315
 
299
316
  // pending subtask