blun-king-cli 9.1.57 → 9.1.60

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/LIESMICH.txt CHANGED
@@ -9,7 +9,7 @@ Installation
9
9
  ------------
10
10
  Die geprüfte Version exakt global installieren:
11
11
 
12
- npm install -g blun-king-cli@9.1.57
12
+ npm install -g blun-king-cli@9.1.59
13
13
 
14
14
  Start
15
15
  -----
package/README.md CHANGED
@@ -9,7 +9,7 @@ Voraussetzung ist Node.js 24.15 oder neuer. Die geprüfte Version wird exakt
9
9
  installiert:
10
10
 
11
11
  ```powershell
12
- npm install -g blun-king-cli@9.1.57
12
+ npm install -g blun-king-cli@9.1.59
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -10,6 +10,8 @@ const LAUNCHER_HELP_TEXT = [
10
10
  'Launchers:',
11
11
  ' blun Starts the CLI without automatically attaching Telegram.',
12
12
  ' king Starts the same CLI and automatically attaches the configured Telegram channel.',
13
+ ' king <profile> starts an isolated named King profile.',
14
+ ' king -c <profile> continues the last session in that profile.',
13
15
  ' Both use the same version, account, model, and commands.',
14
16
  ' blun tools list shows optional tools; enable writes only explicit choices.',
15
17
  '',
@@ -31,9 +31,16 @@ const {
31
31
  securePrivateFile,
32
32
  writePrivateFile,
33
33
  } = require('./private-paths');
34
+ const {
35
+ parseProfileLaunchArgs,
36
+ resolveProfilePaths,
37
+ seedDefaultProfileFromLegacy,
38
+ } = require('./profile-runtime.cjs');
34
39
 
35
40
  const PKG = path.resolve(__dirname, '..');
36
- const ARGS = process.argv.slice(2);
41
+ const RAW_ARGS = process.argv.slice(2);
42
+ const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.argv));
43
+ const ARGS = PROFILE.args;
37
44
  const CORE_LOAD_TIMEOUT_MS = 30_000;
38
45
 
39
46
  function normalizeWindowsPath(value) {
@@ -44,10 +51,24 @@ function resolveLauncherPrivatePaths(options = {}) {
44
51
  const env = options.env || process.env;
45
52
  const platform = options.platform || process.platform;
46
53
  const configuredHome = String(env.BLUN_HOME || '').trim();
54
+ const configuredSharedHome = String(env.BLUN_SHARED_HOME || '').trim();
55
+ const profileName = options.profileName || PROFILE.profileName;
47
56
  if (platform !== 'win32') {
48
57
  const userHome = (options.homedir || os.homedir)();
58
+ const sharedHome = configuredSharedHome
59
+ ? path.resolve(configuredSharedHome)
60
+ : configuredHome
61
+ ? path.resolve(configuredHome)
62
+ : path.join(userHome, '.blun');
63
+ const profilePaths = resolveProfilePaths(sharedHome, profileName);
64
+ if (configuredSharedHome && configuredHome
65
+ && path.resolve(configuredHome) !== path.resolve(profilePaths.home)) {
66
+ throw new Error(`BLUN_HOME passt nicht zum Profil ${profileName}: ${profilePaths.home}`);
67
+ }
49
68
  return {
50
- blunHome: configuredHome ? path.resolve(configuredHome) : path.join(userHome, '.blun'),
69
+ blunHome: profilePaths.home,
70
+ profilePaths,
71
+ sharedHome,
51
72
  userHome,
52
73
  };
53
74
  }
@@ -71,12 +92,26 @@ function resolveLauncherPrivatePaths(options = {}) {
71
92
  throw new Error('Das lokale Windows-Benutzerprofil ist kein kanonischer lokaler Pfad.');
72
93
  }
73
94
 
74
- const blunHome = path.win32.join(userHome, '.blun');
95
+ const sharedHome = path.win32.join(userHome, '.blun');
96
+ const profilePaths = resolveProfilePaths(sharedHome, profileName, path.win32);
97
+ if (configuredSharedHome
98
+ && normalizeWindowsPath(path.win32.resolve(configuredSharedHome)) !== normalizeWindowsPath(sharedHome)) {
99
+ throw new Error(`BLUN_SHARED_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${sharedHome}`);
100
+ }
101
+ const configuredHomeIsLegacyShared = !configuredSharedHome
102
+ && configuredHome
103
+ && normalizeWindowsPath(path.win32.resolve(configuredHome)) === normalizeWindowsPath(sharedHome);
75
104
  if (configuredHome
76
- && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(blunHome)) {
77
- throw new Error(`BLUN_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${blunHome}`);
105
+ && !configuredHomeIsLegacyShared
106
+ && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(profilePaths.home)) {
107
+ throw new Error(`BLUN_HOME passt nicht zum Profil ${profileName}: ${profilePaths.home}`);
78
108
  }
79
- return { blunHome, userHome };
109
+ return {
110
+ blunHome: profilePaths.home,
111
+ profilePaths,
112
+ sharedHome,
113
+ userHome,
114
+ };
80
115
  }
81
116
 
82
117
  function readPackageVersion() {
@@ -271,12 +306,12 @@ async function runLauncher(options = {}) {
271
306
  if (mcpNames.length > 0) {
272
307
  seedStandardTools({
273
308
  packageRoot: PKG,
274
- blunDir: privatePaths.blunHome,
309
+ blunDir: privatePaths.profilePaths.home,
275
310
  names: mcpNames,
276
311
  });
277
312
  }
278
313
  if (selected.includes('telegram')) {
279
- installTelegramForProfile(PKG, privatePaths.blunHome);
314
+ installTelegramForProfile(PKG, privatePaths.profilePaths.home);
280
315
  }
281
316
  process.stdout.write(`${selected.join('\n')}\n`);
282
317
  return;
@@ -298,7 +333,7 @@ async function runLauncher(options = {}) {
298
333
  stdin: process.stdin,
299
334
  stdout: process.stdout,
300
335
  stderr: process.stderr,
301
- blunDir: privatePaths.blunHome,
336
+ blunDir: privatePaths.sharedHome,
302
337
  packageRoot: PKG,
303
338
  noticeLease,
304
339
  request: ARGS[0],
@@ -307,13 +342,13 @@ async function runLauncher(options = {}) {
307
342
  return;
308
343
  }
309
344
 
310
- let privatePaths;
345
+ let privatePathsCache;
311
346
  const getPrivatePaths = () => {
312
- privatePaths ||= resolveLauncherPrivatePaths();
313
- return privatePaths;
347
+ privatePathsCache ||= resolveLauncherPrivatePaths();
348
+ return privatePathsCache;
314
349
  };
315
350
  const nodeRuntime = await prepareManagedNodeRuntime({
316
- getBlunDir: () => getPrivatePaths().blunHome,
351
+ getBlunDir: () => getPrivatePaths().sharedHome,
317
352
  });
318
353
  if (nodeRuntime.kind === 'failed') {
319
354
  process.stderr.write(`${nodeRuntime.message}\n`);
@@ -332,12 +367,24 @@ async function runLauncher(options = {}) {
332
367
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
333
368
  const informationalEnv = { ...process.env };
334
369
  delete informationalEnv.BLUN_HOME;
335
- env.BLUN_HOME = resolveLauncherPrivatePaths({ env: informationalEnv }).blunHome;
370
+ delete informationalEnv.BLUN_SHARED_HOME;
371
+ const privatePaths = resolveLauncherPrivatePaths({ env: informationalEnv });
372
+ const profilePaths = privatePaths.profilePaths;
373
+ env.BLUN_HOME = profilePaths.home;
374
+ env.BLUN_SHARED_HOME = privatePaths.sharedHome;
375
+ env.BLUN_LOG_HOME = privatePaths.sharedHome;
376
+ env.BLUN_PROFILE = PROFILE.profileName;
336
377
  process.exitCode = await superviseProtectedCore(ARGS, env, callerCwd, releaseNotice);
337
378
  return;
338
379
  }
339
380
 
340
- const blunDir = getPrivatePaths().blunHome;
381
+ const privatePaths = getPrivatePaths();
382
+ const profilePaths = privatePaths.profilePaths;
383
+ ensurePrivateDirectory(privatePaths.sharedHome);
384
+ if (PROFILE.profileName === 'default') {
385
+ seedDefaultProfileFromLegacy(privatePaths.sharedHome);
386
+ }
387
+ const blunDir = profilePaths.home;
341
388
  ensurePrivateDirectory(blunDir);
342
389
 
343
390
  // --- 1. Oeffentlicher npm-Update-Dialog. -------------------------------
@@ -350,7 +397,7 @@ async function runLauncher(options = {}) {
350
397
  stdin: process.stdin,
351
398
  stdout: process.stdout,
352
399
  stderr: process.stderr,
353
- blunDir,
400
+ blunDir: privatePaths.sharedHome,
354
401
  packageRoot: PKG,
355
402
  noticeLease,
356
403
  });
@@ -382,7 +429,7 @@ async function runLauncher(options = {}) {
382
429
  });
383
430
 
384
431
  // --- 4. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
385
- seedStandardDesignSkill({ packageRoot: PKG, blunDir });
432
+ seedStandardDesignSkill({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
386
433
 
387
434
  // --- 5. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
388
435
  if (mode === LAUNCHER_MODES.KING) {
@@ -402,7 +449,10 @@ async function runLauncher(options = {}) {
402
449
 
403
450
  // --- 6. Start -----------------------------------------------------------
404
451
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
405
- env.BLUN_HOME = blunDir;
452
+ env.BLUN_HOME = profilePaths.home;
453
+ env.BLUN_SHARED_HOME = privatePaths.sharedHome;
454
+ env.BLUN_LOG_HOME = privatePaths.sharedHome;
455
+ env.BLUN_PROFILE = PROFILE.profileName;
406
456
  env.BLUN_MODEL_MAX_COMPLETION_TOKENS = env.BLUN_MODEL_MAX_COMPLETION_TOKENS || '32768';
407
457
  process.exitCode = await superviseProtectedCore(
408
458
  ARGS,
@@ -34,6 +34,9 @@ public static class BlunPrivateAclNative
34
34
  private const uint READ_CONTROL = 0x00020000;
35
35
  private const uint WRITE_DAC = 0x00040000;
36
36
  private const uint SYNCHRONIZE = 0x00100000;
37
+ private const uint FILE_SHARE_READ = 0x00000001;
38
+ private const uint FILE_SHARE_WRITE = 0x00000002;
39
+ private const uint FILE_SHARE_DELETE = 0x00000004;
37
40
  private const uint CREATE_NEW = 1;
38
41
  private const uint OPEN_EXISTING = 3;
39
42
  private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
@@ -608,7 +611,7 @@ public static class BlunPrivateAclNative
608
611
  out ioStatus,
609
612
  IntPtr.Zero,
610
613
  0,
611
- 0,
614
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
612
615
  FILE_OPEN_IF,
613
616
  FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
614
617
  IntPtr.Zero,
@@ -1271,8 +1274,8 @@ function windowsAclController(options) {
1271
1274
  const detail = rawDetail.replace(/[\u0000-\u001f\u007f]+/gu, ' ').trim().slice(0, 500);
1272
1275
  throw new Error(
1273
1276
  detail.length > 0
1274
- ? `Windows private ACL operation failed: ${detail}`
1275
- : 'Windows private ACL operation failed.',
1277
+ ? `Windows private ACL operation failed for ${kind} target "${absoluteTarget}": ${detail}`
1278
+ : `Windows private ACL operation failed for ${kind} target "${absoluteTarget}".`,
1276
1279
  { cause: error },
1277
1280
  );
1278
1281
  }
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const DEFAULT_PROFILE = 'default';
7
+ const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
8
+ const RESERVED_SINGLE_ARGUMENTS = new Set([
9
+ 'doctor',
10
+ 'export',
11
+ 'help',
12
+ 'mcp',
13
+ 'server',
14
+ 'tools',
15
+ 'update',
16
+ 'upgrade',
17
+ 'version',
18
+ ]);
19
+ const LEGACY_PROFILE_ENTRIES = Object.freeze([
20
+ 'channels',
21
+ 'config.toml',
22
+ 'mcp.json',
23
+ 'plugins',
24
+ 'session_index.jsonl',
25
+ 'sessions',
26
+ 'tui.toml',
27
+ ]);
28
+ const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
29
+
30
+ function normalizeProfileName(value) {
31
+ const normalized = String(value || '').trim().toLowerCase();
32
+ if (!PROFILE_NAME_PATTERN.test(normalized)) {
33
+ throw new Error('Profilnamen duerfen nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.');
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ function parseProfileLaunchArgs(inputArgs, mode = 'blun') {
39
+ const args = Array.isArray(inputArgs) ? [...inputArgs] : [];
40
+ if (String(mode).toLowerCase() !== 'king') {
41
+ return { args, profileName: DEFAULT_PROFILE };
42
+ }
43
+
44
+ const profileFlagIndex = args.indexOf('--profile');
45
+ if (profileFlagIndex >= 0) {
46
+ if (profileFlagIndex + 1 >= args.length) {
47
+ throw new Error('--profile braucht einen Profilnamen.');
48
+ }
49
+ const profileName = normalizeProfileName(args[profileFlagIndex + 1]);
50
+ args.splice(profileFlagIndex, 2);
51
+ return { args, profileName };
52
+ }
53
+
54
+ if (args.length === 1) {
55
+ const candidate = String(args[0] || '');
56
+ if (!candidate.startsWith('-') && !RESERVED_SINGLE_ARGUMENTS.has(candidate.toLowerCase())) {
57
+ return { args: [], profileName: normalizeProfileName(candidate) };
58
+ }
59
+ }
60
+
61
+ if (args.length === 2 && args.includes('-c')) {
62
+ const candidate = args.find((arg) => arg !== '-c');
63
+ if (candidate && !candidate.startsWith('-')) {
64
+ return { args: ['-c'], profileName: normalizeProfileName(candidate) };
65
+ }
66
+ }
67
+
68
+ return { args, profileName: DEFAULT_PROFILE };
69
+ }
70
+
71
+ function resolveProfilePaths(sharedHome, profileName, pathImpl = path) {
72
+ const normalized = normalizeProfileName(profileName);
73
+ const home = pathImpl.join(sharedHome, 'profile', normalized);
74
+ return Object.freeze({
75
+ profileName: normalized,
76
+ sharedHome,
77
+ home,
78
+ channels: pathImpl.join(home, 'channels'),
79
+ sessions: pathImpl.join(home, 'sessions'),
80
+ plugins: pathImpl.join(home, 'plugins'),
81
+ config: pathImpl.join(home, 'config.toml'),
82
+ mcp: pathImpl.join(home, 'mcp.json'),
83
+ sessionIndex: pathImpl.join(home, 'session_index.jsonl'),
84
+ credentials: pathImpl.join(sharedHome, 'credentials'),
85
+ oauth: pathImpl.join(sharedHome, 'oauth'),
86
+ deviceId: pathImpl.join(sharedHome, 'device_id'),
87
+ skills: pathImpl.join(sharedHome, 'skills'),
88
+ secure: pathImpl.join(sharedHome, 'secure'),
89
+ persona: pathImpl.join(sharedHome, 'persona.json'),
90
+ globalLog: pathImpl.join(sharedHome, 'logs', 'blun.log'),
91
+ });
92
+ }
93
+
94
+ function copyLegacyEntry(source, target, fsImpl) {
95
+ if (!fsImpl.existsSync(source) || fsImpl.existsSync(target)) return;
96
+ const stat = fsImpl.lstatSync(source);
97
+ if (stat.isSymbolicLink()) return;
98
+ if (stat.isDirectory()) {
99
+ fsImpl.cpSync(source, target, { recursive: true, errorOnExist: false });
100
+ return;
101
+ }
102
+ if (stat.isFile()) fsImpl.copyFileSync(source, target);
103
+ }
104
+
105
+ function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
106
+ const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
107
+ const marker = path.join(paths.home, MIGRATION_MARKER);
108
+ if (fsImpl.existsSync(marker)) return paths;
109
+
110
+ fsImpl.mkdirSync(paths.home, { recursive: true });
111
+ for (const entry of LEGACY_PROFILE_ENTRIES) {
112
+ copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
113
+ }
114
+ try {
115
+ fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
116
+ } catch (error) {
117
+ if (error?.code !== 'EEXIST') throw error;
118
+ }
119
+ return paths;
120
+ }
121
+
122
+ module.exports = {
123
+ DEFAULT_PROFILE,
124
+ normalizeProfileName,
125
+ parseProfileLaunchArgs,
126
+ resolveProfilePaths,
127
+ seedDefaultProfileFromLegacy,
128
+ };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ const MAX_AUTOMATIC_RATE_LIMIT_RESUMES = 3;
4
+ const RATE_LIMIT_RETRY_BASE_MS = 60_000;
5
+ const RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS = 64_000;
6
+
7
+ function isImmediateGenerateRetryAllowed(statusCode) {
8
+ return statusCode !== 429 && [500, 502, 503, 504].includes(statusCode);
9
+ }
10
+
11
+ function rateLimitRetryDelayMs(retryCount) {
12
+ if (!Number.isInteger(retryCount) || retryCount < 1) {
13
+ throw new Error("retryCount must be a positive integer.");
14
+ }
15
+ return RATE_LIMIT_RETRY_BASE_MS * (2 ** (retryCount - 1));
16
+ }
17
+
18
+ function shouldStopAutomaticRateLimitResume(retryCount) {
19
+ return retryCount >= MAX_AUTOMATIC_RATE_LIMIT_RESUMES;
20
+ }
21
+
22
+ function shouldCompactBeforeRateLimitResume(estimatedRequestTokens) {
23
+ return Number.isFinite(estimatedRequestTokens)
24
+ && estimatedRequestTokens >= RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS;
25
+ }
26
+
27
+ module.exports = {
28
+ MAX_AUTOMATIC_RATE_LIMIT_RESUMES,
29
+ RATE_LIMIT_RETRY_BASE_MS,
30
+ isImmediateGenerateRetryAllowed,
31
+ rateLimitRetryDelayMs,
32
+ shouldCompactBeforeRateLimitResume,
33
+ shouldStopAutomaticRateLimitResume,
34
+ };
@@ -74,6 +74,41 @@ function hasEquivalentServer(servers, name, tool) {
74
74
  });
75
75
  }
76
76
 
77
+ function sameStringArray(left, right) {
78
+ return Array.isArray(left)
79
+ && Array.isArray(right)
80
+ && left.length === right.length
81
+ && left.every((value, index) => value === right[index]);
82
+ }
83
+
84
+ function recognizedBundledServer(current, expected) {
85
+ return isObject(current)
86
+ && current.transport === expected.transport
87
+ && current.command === expected.command
88
+ && sameStringArray(current.args, expected.args);
89
+ }
90
+
91
+ function migrateBundledTimeouts(name, current, expected) {
92
+ if (!recognizedBundledServer(current, expected)) return false;
93
+ let changed = false;
94
+ if (name === 'agent-browser') {
95
+ if (current.startupTimeoutMs === 30_000) {
96
+ current.startupTimeoutMs = expected.startupTimeoutMs;
97
+ changed = true;
98
+ }
99
+ if (current.toolTimeoutMs === 60_000) {
100
+ current.toolTimeoutMs = expected.toolTimeoutMs;
101
+ changed = true;
102
+ }
103
+ }
104
+ if (name === 'blun-language-guard'
105
+ && (current.toolTimeoutMs === 30_000 || current.toolTimeoutMs === 60_000)) {
106
+ current.toolTimeoutMs = expected.toolTimeoutMs;
107
+ changed = true;
108
+ }
109
+ return changed;
110
+ }
111
+
77
112
  function writeMcpDocument(configPath, document) {
78
113
  const temporaryPath = path.join(
79
114
  path.dirname(configPath),
@@ -160,21 +195,27 @@ function seedStandardTools({
160
195
  const document = readMcpDocument(configPath);
161
196
  const servers = { ...document.mcpServers };
162
197
  const added = [];
198
+ const updated = [];
163
199
 
164
200
  for (const name of selectedNames) {
165
201
  const tool = catalogue.tools[name];
166
- if (hasEquivalentServer(servers, name, tool)) continue;
167
- servers[name] = bundledToolConfig(name, tool, {
202
+ const expected = bundledToolConfig(name, tool, {
168
203
  packageRoot,
169
204
  blunDir,
170
205
  platform,
171
206
  env,
172
207
  });
208
+ if (Object.hasOwn(servers, name)) {
209
+ if (migrateBundledTimeouts(name, servers[name], expected)) updated.push(name);
210
+ continue;
211
+ }
212
+ if (hasEquivalentServer(servers, name, tool)) continue;
213
+ servers[name] = expected;
173
214
  added.push(name);
174
215
  }
175
216
 
176
- if (added.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
177
- return { added, configPath };
217
+ if (added.length > 0 || updated.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
218
+ return { added, updated, configPath };
178
219
  }
179
220
 
180
221
  function seedStandardDesignSkill({ packageRoot, blunDir }) {
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_SUBAGENT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
4
+ const MAX_SUBAGENT_TIMEOUT_MINUTES = 7 * 24 * 60;
5
+ const SUBAGENT_TIMEOUT_ENV = "BLUN_SUBAGENT_TIMEOUT_MINUTES";
6
+
7
+ function resolveSubagentTimeoutMs({ perRunMinutes, env = process.env } = {}) {
8
+ const raw = perRunMinutes ?? env[SUBAGENT_TIMEOUT_ENV];
9
+ if (raw === undefined || raw === null || raw === "") {
10
+ return DEFAULT_SUBAGENT_TIMEOUT_MS;
11
+ }
12
+
13
+ const minutes = typeof raw === "number" ? raw : Number(raw);
14
+ if (!Number.isInteger(minutes) || minutes < 0 || minutes > MAX_SUBAGENT_TIMEOUT_MINUTES) {
15
+ throw new Error(
16
+ `${SUBAGENT_TIMEOUT_ENV} must be an integer from 0 to ${MAX_SUBAGENT_TIMEOUT_MINUTES} minutes.`,
17
+ );
18
+ }
19
+ return minutes === 0 ? undefined : minutes * 60 * 1000;
20
+ }
21
+
22
+ function abortError(reason) {
23
+ if (reason instanceof Error) return reason;
24
+ return new Error(reason === undefined ? "Subagent interrupted." : String(reason));
25
+ }
26
+
27
+ async function runAgentWithTimeoutContinuation({
28
+ handle,
29
+ timeoutMs,
30
+ signal,
31
+ abortCurrent,
32
+ retrySameAgent,
33
+ }) {
34
+ let currentHandle = handle;
35
+
36
+ while (true) {
37
+ let timeout;
38
+ let removeAbortListener = () => {};
39
+ const completion = Promise.resolve(currentHandle.completion).then(
40
+ (outcome) => ({ kind: "completed", outcome }),
41
+ (error) => ({ kind: "failed", error }),
42
+ );
43
+ const timedOut = timeoutMs === undefined
44
+ ? new Promise(() => {})
45
+ : new Promise((resolve) => {
46
+ timeout = setTimeout(() => resolve({ kind: "timed_out" }), timeoutMs);
47
+ });
48
+ const interrupted = signal.aborted
49
+ ? Promise.resolve({ kind: "interrupted", reason: signal.reason })
50
+ : new Promise((resolve) => {
51
+ const onAbort = () => resolve({ kind: "interrupted", reason: signal.reason });
52
+ signal.addEventListener("abort", onAbort, { once: true });
53
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
54
+ });
55
+
56
+ const result = await Promise.race([completion, timedOut, interrupted]);
57
+ if (timeout !== undefined) clearTimeout(timeout);
58
+ removeAbortListener();
59
+
60
+ if (result.kind === "completed") return { handle: currentHandle, outcome: result.outcome };
61
+ if (result.kind === "failed") throw result.error;
62
+ if (result.kind === "interrupted") {
63
+ abortCurrent(result.reason);
64
+ await Promise.resolve(currentHandle.completion).catch(() => {});
65
+ throw abortError(result.reason);
66
+ }
67
+
68
+ const reason = new Error("Subagent continuation interval elapsed.");
69
+ abortCurrent(reason);
70
+ await Promise.resolve(currentHandle.completion).catch(() => {});
71
+ currentHandle = await retrySameAgent(currentHandle.agentId);
72
+ }
73
+ }
74
+
75
+ module.exports = {
76
+ DEFAULT_SUBAGENT_TIMEOUT_MS,
77
+ resolveSubagentTimeoutMs,
78
+ runAgentWithTimeoutContinuation,
79
+ };