blun-king-cli 9.1.101 → 9.1.103

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
@@ -103,6 +103,8 @@ Das Telemetrieereignis `tool_result_batch_offloaded` meldet ausschließlich die
103
103
 
104
104
  Reguläre `Agent`-Teilaufgaben verwenden eine eigene `ContextMemory` und eine eigene `wire.jsonl` in einem getrennten Agentenverzeichnis. Der Hauptagent erhält nur die Ergebniszusammenfassung, sodass lange Teilaufgaben nicht den Hauptverlauf füllen. `TodoList` speichert Pläne weiterhin maschinenlesbar im Sitzungs-Wire; `blun handoff` übergibt sie zusammen mit prüfbaren Hashes zwischen CLI, Desktop und Web.
105
105
 
106
+ Verliert ein delegierter Einzelagent oder ein Mitglied eines Agentenschwarms die Provider-Verbindung, erhält es eine leere Provider-Antwort, bricht sein Stream wegen Leerlaufs ab oder meldet der Server HTTP 408/500/502/503/504, setzt BLUN King denselben Agenten anhand seines dauerhaften Verlaufs fort. Bei Einzelagenten sowie bei Schwarm-Unterbrechungen außerhalb von HTTP 429 wartet die erste Fortsetzung zwei Sekunden und die zweite fünf Sekunden; danach bleibt der ursprüngliche Fehler sichtbar. HTTP 429 behält im Schwarm seine getrennte, längere Überlastungsregel. Authentifizierungs-, Zahlungs- oder Kontingent-, Richtlinien-, Kontext-, Werkzeug- und Codefehler sowie manuelle Abbrüche lösen keine automatische Fortsetzung aus. Jeder Fortsetzungsversuch verwendet die bestehende Agenten-ID, damit bereits abgeschlossene Arbeit nicht wiederholt wird.
107
+
106
108
  Rein lesende Sitzungsdiagnose
107
109
  --------------------------------
108
110
 
@@ -3,6 +3,26 @@
3
3
  const DEFAULT_SUBAGENT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
4
4
  const MAX_SUBAGENT_TIMEOUT_MINUTES = 7 * 24 * 60;
5
5
  const SUBAGENT_TIMEOUT_ENV = "BLUN_SUBAGENT_TIMEOUT_MINUTES";
6
+ const MAX_TRANSIENT_SUBAGENT_CONTINUATIONS = 2;
7
+ const DEFAULT_TRANSIENT_RETRY_DELAYS_MS = Object.freeze([2000, 5000]);
8
+ const TERMINAL_ERROR_CODES = new Set([
9
+ "auth.login_required",
10
+ "context.overflow",
11
+ "provider.auth_error",
12
+ "provider.quota_exhausted",
13
+ ]);
14
+ const RETRYABLE_ERROR_CODES = new Set([
15
+ "compaction.stalled",
16
+ "provider.connection_error",
17
+ "provider.rate_limit",
18
+ ]);
19
+ const RETRYABLE_ERROR_NAMES = new Set([
20
+ "APIConnectionError",
21
+ "APIEmptyResponseError",
22
+ "APITimeoutError",
23
+ "CompactionStallError",
24
+ ]);
25
+ const RETRYABLE_MESSAGE_RE = /(?:\[provider\.(?:connection_error|rate_limit)\]|\[compaction\.stalled\]|no provider data arrived|stream was aborted to prevent an indefinite hang|\b(?:econnreset|econnrefused|etimedout)\b|fetch failed|socket hang up|connection (?:closed|reset|terminated|timed out)|network error)/iu;
6
26
 
7
27
  function resolveSubagentTimeoutMs({ perRunMinutes, env = process.env } = {}) {
8
28
  const raw = perRunMinutes ?? env[SUBAGENT_TIMEOUT_ENV];
@@ -24,14 +44,75 @@ function abortError(reason) {
24
44
  return new Error(reason === undefined ? "Subagent interrupted." : String(reason));
25
45
  }
26
46
 
47
+ function errorField(error, field) {
48
+ return typeof error === "object" && error !== null ? error[field] : undefined;
49
+ }
50
+
51
+ function isRetryableSubagentFailure(error) {
52
+ const code = errorField(error, "code");
53
+ const statusCode = errorField(error, "statusCode") ?? errorField(error, "status");
54
+ const name = errorField(error, "name");
55
+ const message = error instanceof Error ? error.message : String(error);
56
+
57
+ if (typeof code === "string" && TERMINAL_ERROR_CODES.has(code)) return false;
58
+ if (statusCode === 401 || statusCode === 402 || statusCode === 403
59
+ || statusCode === 413 || statusCode === 422) return false;
60
+ if (/safety policy|filtered|payment|required quota|context (?:window )?(?:overflow|too long)/iu.test(message)) {
61
+ return false;
62
+ }
63
+ if (typeof code === "string" && RETRYABLE_ERROR_CODES.has(code)) return true;
64
+ if (typeof name === "string" && RETRYABLE_ERROR_NAMES.has(name)) return true;
65
+ if (statusCode === 408 || statusCode === 429
66
+ || statusCode === 500 || statusCode === 502 || statusCode === 503 || statusCode === 504) {
67
+ return true;
68
+ }
69
+ if (errorField(error, "retryable") === true
70
+ && typeof code === "string"
71
+ && (code.startsWith("provider.") || code.startsWith("compaction."))) {
72
+ return true;
73
+ }
74
+ return RETRYABLE_MESSAGE_RE.test(message);
75
+ }
76
+
77
+ function transientSubagentRetryDelayMs(retryCount) {
78
+ if (!Number.isInteger(retryCount) || retryCount < 1) {
79
+ throw new Error("retryCount must be a positive integer.");
80
+ }
81
+ return DEFAULT_TRANSIENT_RETRY_DELAYS_MS[retryCount - 1]
82
+ ?? DEFAULT_TRANSIENT_RETRY_DELAYS_MS.at(-1);
83
+ }
84
+
85
+ function shouldStopTransientSubagentContinuation(retryCount) {
86
+ return retryCount >= MAX_TRANSIENT_SUBAGENT_CONTINUATIONS;
87
+ }
88
+
89
+ function waitForRetryDelay(delayMs, signal) {
90
+ if (delayMs <= 0) return Promise.resolve();
91
+ if (signal.aborted) return Promise.reject(abortError(signal.reason));
92
+ return new Promise((resolve, reject) => {
93
+ const timeout = setTimeout(() => {
94
+ signal.removeEventListener("abort", onAbort);
95
+ resolve();
96
+ }, delayMs);
97
+ const onAbort = () => {
98
+ clearTimeout(timeout);
99
+ signal.removeEventListener("abort", onAbort);
100
+ reject(abortError(signal.reason));
101
+ };
102
+ signal.addEventListener("abort", onAbort, { once: true });
103
+ });
104
+ }
105
+
27
106
  async function runAgentWithTimeoutContinuation({
28
107
  handle,
29
108
  timeoutMs,
30
109
  signal,
31
110
  abortCurrent,
32
111
  retrySameAgent,
112
+ retryDelaysMs = DEFAULT_TRANSIENT_RETRY_DELAYS_MS,
33
113
  }) {
34
114
  let currentHandle = handle;
115
+ let transientContinuations = 0;
35
116
 
36
117
  while (true) {
37
118
  let timeout;
@@ -58,7 +139,25 @@ async function runAgentWithTimeoutContinuation({
58
139
  removeAbortListener();
59
140
 
60
141
  if (result.kind === "completed") return { handle: currentHandle, outcome: result.outcome };
61
- if (result.kind === "failed") throw result.error;
142
+ if (result.kind === "failed") {
143
+ if (signal.aborted) throw abortError(signal.reason);
144
+ if (!isRetryableSubagentFailure(result.error)
145
+ || transientContinuations >= MAX_TRANSIENT_SUBAGENT_CONTINUATIONS) {
146
+ throw result.error;
147
+ }
148
+ const attempt = transientContinuations + 1;
149
+ const delayMs = retryDelaysMs[transientContinuations]
150
+ ?? transientSubagentRetryDelayMs(attempt);
151
+ transientContinuations = attempt;
152
+ await waitForRetryDelay(delayMs, signal);
153
+ currentHandle = await retrySameAgent(currentHandle.agentId, {
154
+ kind: "transient_provider",
155
+ attempt,
156
+ maxAttempts: MAX_TRANSIENT_SUBAGENT_CONTINUATIONS,
157
+ error: result.error,
158
+ });
159
+ continue;
160
+ }
62
161
  if (result.kind === "interrupted") {
63
162
  abortCurrent(result.reason);
64
163
  await Promise.resolve(currentHandle.completion).catch(() => {});
@@ -68,12 +167,16 @@ async function runAgentWithTimeoutContinuation({
68
167
  const reason = new Error("Subagent continuation interval elapsed.");
69
168
  abortCurrent(reason);
70
169
  await Promise.resolve(currentHandle.completion).catch(() => {});
71
- currentHandle = await retrySameAgent(currentHandle.agentId);
170
+ currentHandle = await retrySameAgent(currentHandle.agentId, { kind: "wall_clock" });
72
171
  }
73
172
  }
74
173
 
75
174
  module.exports = {
76
175
  DEFAULT_SUBAGENT_TIMEOUT_MS,
176
+ MAX_TRANSIENT_SUBAGENT_CONTINUATIONS,
177
+ isRetryableSubagentFailure,
77
178
  resolveSubagentTimeoutMs,
78
179
  runAgentWithTimeoutContinuation,
180
+ shouldStopTransientSubagentContinuation,
181
+ transientSubagentRetryDelayMs,
79
182
  };
package/blun.mjs CHANGED
@@ -29058,11 +29058,11 @@ var init_agent_task = __esmMin((() => {
29058
29058
  abortCurrent: (reason) => {
29059
29059
  this.abortController.abort(reason);
29060
29060
  },
29061
- retrySameAgent: async (agentId) => {
29061
+ retrySameAgent: async (agentId, continuation) => {
29062
29062
  this.abortController = new AbortController();
29063
29063
  this.handle = await this.subagentHost.retry(agentId, {
29064
29064
  parentToolCallId: this.parentToolCallId,
29065
- prompt: "Continue from the preserved context after the wall-clock interval.",
29065
+ prompt: continuation.kind === "transient_provider" ? "Continue from the preserved context after a transient provider interruption. Do not repeat completed work; inspect the preserved wire and continue from the last durable boundary." : "Continue from the preserved context after the wall-clock interval.",
29066
29066
  description: this.description,
29067
29067
  runInBackground: this.runInBackground,
29068
29068
  signal: this.abortController.signal
@@ -251353,18 +251353,20 @@ function resolveSwarmMaxConcurrency(env = process.env) {
251353
251353
  if (!Number.isInteger(value) || value <= 0) throw new Error(`${AGENT_SWARM_MAX_CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`);
251354
251354
  return value;
251355
251355
  }
251356
- var import_retry, INITIAL_LAUNCH_LIMIT, INITIAL_LAUNCH_INTERVAL_MS, RATE_LIMIT_RETRY_BASE_MS, RATE_LIMIT_RETRY_FACTOR, RATE_LIMIT_CAPACITY_SHRINK_INTERVAL_MS, RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS, RATE_LIMIT_SUSPENDED_REASON, AGENT_SWARM_MAX_CONCURRENCY_ENV, MAX_AUTOMATIC_RATE_LIMIT_RESUMES, rateLimitRetryDelayMs, shouldCompactBeforeRateLimitResume, shouldStopAutomaticRateLimitResume, SubagentBatch;
251356
+ var import_retry, INITIAL_LAUNCH_LIMIT, INITIAL_LAUNCH_INTERVAL_MS, RATE_LIMIT_RETRY_BASE_MS, RATE_LIMIT_RETRY_FACTOR, RATE_LIMIT_CAPACITY_SHRINK_INTERVAL_MS, RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS, RATE_LIMIT_SUSPENDED_REASON, TRANSIENT_PROVIDER_SUSPENDED_REASON, AGENT_SWARM_MAX_CONCURRENCY_ENV, MAX_AUTOMATIC_RATE_LIMIT_RESUMES, MAX_TRANSIENT_SUBAGENT_CONTINUATIONS, isRetryableSubagentFailure, rateLimitRetryDelayMs, shouldCompactBeforeRateLimitResume, shouldStopAutomaticRateLimitResume, shouldStopTransientSubagentContinuation, transientSubagentRetryDelayMs, SubagentBatch;
251357
251357
  var init_subagent_batch = __esmMin((() => {
251358
251358
  init_src$4();
251359
251359
  import_retry = /* @__PURE__ */ __toESM(require_retry$1(), 1);
251360
251360
  init_abort();
251361
251361
  ({ MAX_AUTOMATIC_RATE_LIMIT_RESUMES, RATE_LIMIT_RETRY_BASE_MS, rateLimitRetryDelayMs, shouldCompactBeforeRateLimitResume, shouldStopAutomaticRateLimitResume } = createRequire(import.meta.url)("./bin/rate-limit-recovery-policy.cjs"));
251362
+ ({ MAX_TRANSIENT_SUBAGENT_CONTINUATIONS, isRetryableSubagentFailure, shouldStopTransientSubagentContinuation, transientSubagentRetryDelayMs } = createRequire(import.meta.url)("./bin/subagent-timeout-policy.cjs"));
251362
251363
  INITIAL_LAUNCH_LIMIT = 5;
251363
251364
  INITIAL_LAUNCH_INTERVAL_MS = 700;
251364
251365
  RATE_LIMIT_RETRY_FACTOR = 2;
251365
251366
  RATE_LIMIT_CAPACITY_SHRINK_INTERVAL_MS = 2e3;
251366
251367
  RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS = 180 * 1e3;
251367
251368
  RATE_LIMIT_SUSPENDED_REASON = "Provider rate limit; subagent requeued for retry.";
251369
+ TRANSIENT_PROVIDER_SUSPENDED_REASON = "Transient provider interruption; subagent requeued for continuation.";
251368
251370
  AGENT_SWARM_MAX_CONCURRENCY_ENV = "BLUN_AGENT_SWARM_MAX_CONCURRENCY";
251369
251371
  SubagentBatch = class {
251370
251372
  launcher;
@@ -251379,6 +251381,7 @@ var init_subagent_batch = __esmMin((() => {
251379
251381
  normalLaunchCount = 0;
251380
251382
  normalLaunchTimer;
251381
251383
  rateLimitLaunchTimer;
251384
+ transientRetryTimers = /* @__PURE__ */ new Set();
251382
251385
  resolve;
251383
251386
  reject;
251384
251387
  finished = false;
@@ -251398,6 +251401,7 @@ var init_subagent_batch = __esmMin((() => {
251398
251401
  index,
251399
251402
  task,
251400
251403
  retryCount: 0,
251404
+ transientRetryCount: 0,
251401
251405
  retryReadyAt: 0,
251402
251406
  started: false
251403
251407
  }));
@@ -251540,6 +251544,11 @@ var init_subagent_batch = __esmMin((() => {
251540
251544
  agentId: handle.agentId,
251541
251545
  error: this.attemptErrorMessage(attempt, error, "failed")
251542
251546
  };
251547
+ if (isRetryableSubagentFailure(error)) return {
251548
+ type: "transient_provider",
251549
+ agentId: handle.agentId,
251550
+ error: this.attemptErrorMessage(attempt, error, "failed")
251551
+ };
251543
251552
  return this.failedAttemptOutcome(attempt, error);
251544
251553
  }
251545
251554
  }
@@ -251571,6 +251580,14 @@ var init_subagent_batch = __esmMin((() => {
251571
251580
  this.requeueTimedOut(attempt, outcome.agentId);
251572
251581
  }
251573
251582
  else if ("status" in outcome) this.results[attempt.state.index] = outcome;
251583
+ else if (outcome.type === "transient_provider" && shouldStopTransientSubagentContinuation(attempt.state.transientRetryCount)) this.results[attempt.state.index] = {
251584
+ task: attempt.state.task,
251585
+ agentId: outcome.agentId,
251586
+ status: "failed",
251587
+ state: "started",
251588
+ error: `${outcome.error} Automatic continuation stopped after ${String(MAX_TRANSIENT_SUBAGENT_CONTINUATIONS)} attempts; agent ${outcome.agentId} remains resumable.`
251589
+ };
251590
+ else if (outcome.type === "transient_provider") this.requeueTransientProvider(attempt, outcome.agentId);
251574
251591
  else if (shouldStopAutomaticRateLimitResume(attempt.state.retryCount)) this.results[attempt.state.index] = {
251575
251592
  task: attempt.state.task,
251576
251593
  agentId: outcome.agentId,
@@ -251604,6 +251621,26 @@ var init_subagent_batch = __esmMin((() => {
251604
251621
  attempt.cleanup();
251605
251622
  return true;
251606
251623
  }
251624
+ requeueTransientProvider(attempt, agentId) {
251625
+ const state = attempt.state;
251626
+ state.agentId = agentId;
251627
+ state.retryAgentId = agentId;
251628
+ state.transientRetryCount += 1;
251629
+ this.launcher.suspended?.({
251630
+ task: state.task,
251631
+ agentId,
251632
+ reason: TRANSIENT_PROVIDER_SUSPENDED_REASON
251633
+ });
251634
+ const retryDelay = transientSubagentRetryDelayMs(state.transientRetryCount);
251635
+ const timer = setTimeout(() => {
251636
+ this.transientRetryTimers.delete(timer);
251637
+ if (this.finished || this.controller.signal.aborted) return;
251638
+ state.retryReadyAt = 0;
251639
+ this.pending.unshift(state);
251640
+ this.schedule();
251641
+ }, retryDelay);
251642
+ this.transientRetryTimers.add(timer);
251643
+ }
251607
251644
  requeueRateLimited(attempt, agentId) {
251608
251645
  const state = attempt.state;
251609
251646
  state.agentId = agentId;
@@ -251714,6 +251751,8 @@ var init_subagent_batch = __esmMin((() => {
251714
251751
  this.batchSignal?.removeEventListener("abort", this.batchAbortListener);
251715
251752
  this.clearNormalTimer();
251716
251753
  this.clearRateLimitTimer();
251754
+ for (const timer of this.transientRetryTimers) clearTimeout(timer);
251755
+ this.transientRetryTimers.clear();
251717
251756
  for (const attempt of this.active.values()) attempt.cleanup();
251718
251757
  this.active.clear();
251719
251758
  }
@@ -251769,7 +251808,14 @@ async function runChildTurnToCompletion(child, signal) {
251769
251808
  if (turnEnded.reason !== "completed") {
251770
251809
  if (turnEnded.reason === "filtered") throw new Error("Subagent turn blocked by provider safety policy");
251771
251810
  if (turnEnded.error?.code === ErrorCodes.PROVIDER_RATE_LIMIT) throw providerRateLimitErrorFromPayload(turnEnded.error);
251772
- throw new Error(turnEnded.error === void 0 ? `Subagent turn ${turnEnded.reason}` : `[${turnEnded.error.code}] ${turnEnded.error.message}`);
251811
+ if (turnEnded.error === void 0) throw new Error(`Subagent turn ${turnEnded.reason}`);
251812
+ const error = new Error(`[${turnEnded.error.code}] ${turnEnded.error.message}`);
251813
+ error.name = turnEnded.error.name ?? "Error";
251814
+ error.code = turnEnded.error.code;
251815
+ error.retryable = turnEnded.error.retryable;
251816
+ const statusCode = turnEnded.error.details?.["statusCode"];
251817
+ if (typeof statusCode === "number") error.statusCode = statusCode;
251818
+ throw error;
251773
251819
  }
251774
251820
  if (completion.stopReason === "max_tokens") throw new Error(`${SUBAGENT_MAX_TOKENS_ERROR}.`);
251775
251821
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.101",
3
+ "version": "9.1.103",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {