blun-king-cli 9.1.101 → 9.1.102

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 die Provider-Verbindung, erhält er eine leere Provider-Antwort, trifft er auf einen Leerlaufabbruch des Streams oder meldet der Server einen wiederholbaren Status, setzt BLUN King denselben Agenten anhand seines dauerhaften Verlaufs fort. Die erste Fortsetzung wartet zwei Sekunden, die zweite fünf Sekunden; danach bleibt der ursprüngliche Fehler sichtbar. Die Fortsetzungsanweisung fordert den Agenten ausdrücklich auf, den erhaltenen Arbeitsstand zu prüfen und abgeschlossene Schritte nicht zu wiederholen. Authentifizierungs-, Guthaben-, Sicherheits-, Kontext- sowie beliebige Code- und Werkzeugfehler und manuelle Abbrüche bleiben endgültig. Dieser Schutz gilt für einen delegierten Einzelagenten; für Schwärme gelten weiterhin deren bestehende Zeitgrenzen- und Überlastungsregeln.
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,63 @@ 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 waitForRetryDelay(delayMs, signal) {
78
+ if (delayMs <= 0) return Promise.resolve();
79
+ if (signal.aborted) return Promise.reject(abortError(signal.reason));
80
+ return new Promise((resolve, reject) => {
81
+ const timeout = setTimeout(() => {
82
+ signal.removeEventListener("abort", onAbort);
83
+ resolve();
84
+ }, delayMs);
85
+ const onAbort = () => {
86
+ clearTimeout(timeout);
87
+ signal.removeEventListener("abort", onAbort);
88
+ reject(abortError(signal.reason));
89
+ };
90
+ signal.addEventListener("abort", onAbort, { once: true });
91
+ });
92
+ }
93
+
27
94
  async function runAgentWithTimeoutContinuation({
28
95
  handle,
29
96
  timeoutMs,
30
97
  signal,
31
98
  abortCurrent,
32
99
  retrySameAgent,
100
+ retryDelaysMs = DEFAULT_TRANSIENT_RETRY_DELAYS_MS,
33
101
  }) {
34
102
  let currentHandle = handle;
103
+ let transientContinuations = 0;
35
104
 
36
105
  while (true) {
37
106
  let timeout;
@@ -58,7 +127,24 @@ async function runAgentWithTimeoutContinuation({
58
127
  removeAbortListener();
59
128
 
60
129
  if (result.kind === "completed") return { handle: currentHandle, outcome: result.outcome };
61
- if (result.kind === "failed") throw result.error;
130
+ if (result.kind === "failed") {
131
+ if (signal.aborted) throw abortError(signal.reason);
132
+ if (!isRetryableSubagentFailure(result.error)
133
+ || transientContinuations >= MAX_TRANSIENT_SUBAGENT_CONTINUATIONS) {
134
+ throw result.error;
135
+ }
136
+ const attempt = transientContinuations + 1;
137
+ const delayMs = retryDelaysMs[transientContinuations] ?? retryDelaysMs.at(-1) ?? 0;
138
+ transientContinuations = attempt;
139
+ await waitForRetryDelay(delayMs, signal);
140
+ currentHandle = await retrySameAgent(currentHandle.agentId, {
141
+ kind: "transient_provider",
142
+ attempt,
143
+ maxAttempts: MAX_TRANSIENT_SUBAGENT_CONTINUATIONS,
144
+ error: result.error,
145
+ });
146
+ continue;
147
+ }
62
148
  if (result.kind === "interrupted") {
63
149
  abortCurrent(result.reason);
64
150
  await Promise.resolve(currentHandle.completion).catch(() => {});
@@ -68,12 +154,14 @@ async function runAgentWithTimeoutContinuation({
68
154
  const reason = new Error("Subagent continuation interval elapsed.");
69
155
  abortCurrent(reason);
70
156
  await Promise.resolve(currentHandle.completion).catch(() => {});
71
- currentHandle = await retrySameAgent(currentHandle.agentId);
157
+ currentHandle = await retrySameAgent(currentHandle.agentId, { kind: "wall_clock" });
72
158
  }
73
159
  }
74
160
 
75
161
  module.exports = {
76
162
  DEFAULT_SUBAGENT_TIMEOUT_MS,
163
+ MAX_TRANSIENT_SUBAGENT_CONTINUATIONS,
164
+ isRetryableSubagentFailure,
77
165
  resolveSubagentTimeoutMs,
78
166
  runAgentWithTimeoutContinuation,
79
167
  };
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
@@ -251769,7 +251769,14 @@ async function runChildTurnToCompletion(child, signal) {
251769
251769
  if (turnEnded.reason !== "completed") {
251770
251770
  if (turnEnded.reason === "filtered") throw new Error("Subagent turn blocked by provider safety policy");
251771
251771
  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}`);
251772
+ if (turnEnded.error === void 0) throw new Error(`Subagent turn ${turnEnded.reason}`);
251773
+ const error = new Error(`[${turnEnded.error.code}] ${turnEnded.error.message}`);
251774
+ error.name = turnEnded.error.name ?? "Error";
251775
+ error.code = turnEnded.error.code;
251776
+ error.retryable = turnEnded.error.retryable;
251777
+ const statusCode = turnEnded.error.details?.["statusCode"];
251778
+ if (typeof statusCode === "number") error.statusCode = statusCode;
251779
+ throw error;
251773
251780
  }
251774
251781
  if (completion.stopReason === "max_tokens") throw new Error(`${SUBAGENT_MAX_TOKENS_ERROR}.`);
251775
251782
  }
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.102",
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": {