blun-king-cli 9.1.50 → 9.1.52

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.50
12
+ npm install -g blun-king-cli@9.1.52
13
13
 
14
14
  Start
15
15
  -----
@@ -20,6 +20,10 @@ Start
20
20
  Beide Startbefehle verwenden dieselbe Version, dasselbe Konto, dasselbe Modell
21
21
  und dieselben Befehle.
22
22
 
23
+ Beim Fortsetzen mit --continue oder --session wird der Arbeitsbereich der
24
+ bestehenden Sitzung direkt verwendet. Die Ordnerauswahl bleibt neuen Starts
25
+ vorbehalten und kann eine Fortsetzung daher nicht mehr vorzeitig beenden.
26
+
23
27
  Zug-Wächter
24
28
  -----------
25
29
  Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
@@ -41,6 +45,25 @@ Ein neuer /loop ersetzt den bisherigen Loop derselben Sitzung. Dabei bleibt
41
45
  genau ein gespeicherter Loop aktiv. /loop stop, /loop pause und /loop status
42
46
  wirken auch dann sofort, wenn der Agent gerade arbeitet.
43
47
 
48
+ Eine fällige Wiederholung wird als autonomer Aufwecker eingespeist und nicht
49
+ als neue Nutzernachricht behandelt. Die Statuszeile zeigt den nächsten
50
+ Aufweckzeitpunkt laufend an. Beim Fortsetzen derselben Sitzung wird der
51
+ gespeicherte Loop samt Zeitplan wieder geladen; /new beginnt dagegen bewusst
52
+ ohne den Loop der vorherigen Sitzung.
53
+
54
+ Eigenständige Ideenarbeit und Telegram-Befehle
55
+ -----------------------------------------------
56
+ /idea <Ziel> startet einen eigenständigen Arbeitsmodus. Der Agent zeigt zuerst
57
+ einen auftragsspezifischen Aufgabenplan, recherchiert selbstständig, setzt sichere
58
+ lokale Schritte um und fragt gezielt nach, wenn eine Entscheidung oder ein Zugang
59
+ fehlt. Blockierte, auf Freigabe wartende und abgebrochene Schritte bleiben mit
60
+ ihrem tatsächlichen Zustand sichtbar.
61
+
62
+ Im automatisch angebundenen Telegram-Kanal werden ausschließlich /loop, /goal,
63
+ /idea und /befehle als Befehle ausgeführt. Ist der Agent beschäftigt, bleiben
64
+ sie in der Warteschlange. Andere Slash-Eingaben werden aus Sicherheitsgründen
65
+ als normale Chatnachrichten behandelt.
66
+
44
67
  Nachweisbare Arbeitsabläufe
45
68
  ---------------------------
46
69
 
@@ -56,11 +79,16 @@ Alle Befehle funktionieren identisch mit `king`.
56
79
 
57
80
  Medienerzeugung
58
81
  ---------------
59
- GenerateVideo erzeugt Videos aus einer Textbeschreibung oder animiert einen
60
- bereits abgeschlossenen Bildauftrag. Für Bild-zu-Video wird die Medienkennung
61
- des fertigen PNG- oder JPEG-Bildes verwendet; lokale Serverpfade werden nicht
62
- akzeptiert. Der Auftrag läuft asynchron und wird anschließend mit GetMedia
63
- abgerufen.
82
+ GenerateVideo erzeugt Videos aus einer Textbeschreibung, animiert einen
83
+ abgeschlossenen Bildauftrag oder lädt eine lokale PNG- beziehungsweise JPEG-Datei
84
+ hoch und animiert genau dieses Bild. Der Auftrag läuft asynchron und wird
85
+ anschließend mit GetMedia abgerufen.
86
+
87
+ Die Medienleiste erscheint nur bei aktiven Medienaufträgen. Sie zeigt
88
+ Warteschlangenplatz, Produktionsphase und Laufzeit sowie nur tatsächlich vom
89
+ Anbieter gemeldete Prozentwerte, Schritte, Frames, FPS, Audiolänge, Restzeit,
90
+ Auflösung, Wellenform und Vorschauen. Aktualisierte Vorschaubilder werden
91
+ erneuert; Medienpfade und Webadressen sind als Terminalverweise anklickbar.
64
92
 
65
93
  Aktualisieren
66
94
  -------------
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.50
12
+ npm install -g blun-king-cli@9.1.52
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -35,6 +35,10 @@ an. Version, Konto, Modell und Befehle sind ansonsten identisch. Der Unterschied
35
35
  gilt nur für den laufenden Prozess; die gespeicherte
36
36
  Plugin-Konfiguration wird nicht umgeschrieben.
37
37
 
38
+ Beim Fortsetzen mit `--continue` oder `--session` wird der Arbeitsbereich der
39
+ bestehenden Sitzung direkt verwendet. Die Ordnerauswahl bleibt neuen Starts
40
+ vorbehalten und kann eine Fortsetzung daher nicht mehr vorzeitig beenden.
41
+
38
42
  ## Zug-Wächter
39
43
 
40
44
  Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
@@ -56,6 +60,25 @@ genau ein gespeicherter Loop aktiv; der neue Auftrag, das neue Intervall und
56
60
  der neue Startzeitpunkt gelten vollständig. `/loop stop`, `/loop pause` und
57
61
  `/loop status` wirken auch dann sofort, wenn der Agent gerade arbeitet.
58
62
 
63
+ Eine fällige Wiederholung wird als autonomer Aufwecker eingespeist und nicht
64
+ als neue Nutzernachricht behandelt. Die Statuszeile zeigt den nächsten
65
+ Aufweckzeitpunkt laufend an. Beim Fortsetzen derselben Sitzung wird der
66
+ gespeicherte Loop samt Zeitplan wieder geladen; `/new` beginnt dagegen bewusst
67
+ ohne den Loop der vorherigen Sitzung.
68
+
69
+ ## Eigenständige Ideenarbeit und Telegram-Befehle
70
+
71
+ `/idea <Ziel>` startet einen eigenständigen Arbeitsmodus. Der Agent zeigt zuerst
72
+ einen auftragsspezifischen Aufgabenplan, recherchiert selbstständig, setzt sichere
73
+ lokale Schritte um und fragt gezielt nach, wenn eine Entscheidung oder ein Zugang
74
+ fehlt. Blockierte, auf Freigabe wartende und abgebrochene Schritte bleiben mit
75
+ ihrem tatsächlichen Zustand sichtbar.
76
+
77
+ Im automatisch angebundenen Telegram-Kanal werden ausschließlich `/loop`,
78
+ `/goal`, `/idea` und `/befehle` als Befehle ausgeführt. Ist der Agent
79
+ beschäftigt, bleiben sie in der Warteschlange. Andere Slash-Eingaben werden aus
80
+ Sicherheitsgründen als normale Chatnachrichten behandelt.
81
+
59
82
  Beim ersten Start werden das Telegram-Plugin und die mitgelieferten Skills
60
83
  eingerichtet. Die Anmeldung erfolgt anschließend in der
61
84
  Konsole mit `/login` über den BLUN-OAuth-Server. Das Paket erzeugt keine
@@ -77,11 +100,16 @@ Sie stehen unter `blun` und `king` identisch zur Verfügung.
77
100
 
78
101
  ## Medienerzeugung
79
102
 
80
- `GenerateVideo` erzeugt Videos aus einer Textbeschreibung oder animiert einen
81
- bereits abgeschlossenen Bildauftrag. Für Bild-zu-Video wird die Medienkennung
82
- des fertigen PNG- oder JPEG-Bildes verwendet; lokale Serverpfade werden nicht
83
- akzeptiert. Der Auftrag läuft asynchron und wird anschließend mit `GetMedia`
84
- abgerufen.
103
+ `GenerateVideo` erzeugt Videos aus einer Textbeschreibung, animiert einen
104
+ abgeschlossenen Bildauftrag oder lädt eine lokale PNG- beziehungsweise JPEG-Datei
105
+ hoch und animiert genau dieses Bild. Der Auftrag läuft asynchron und wird
106
+ anschließend mit `GetMedia` abgerufen.
107
+
108
+ Die Medienleiste erscheint nur bei aktiven Medienaufträgen. Sie zeigt
109
+ Warteschlangenplatz, Produktionsphase und Laufzeit sowie nur tatsächlich vom
110
+ Anbieter gemeldete Prozentwerte, Schritte, Frames, FPS, Audiolänge, Restzeit,
111
+ Auflösung, Wellenform und Vorschauen. Aktualisierte Vorschaubilder werden
112
+ erneuert; Medienpfade und Webadressen sind als Terminalverweise anklickbar.
85
113
 
86
114
  ## Aktualisieren
87
115
 
package/blun.mjs CHANGED
@@ -30392,7 +30392,7 @@ var init_input_schema = __esmMin((() => {
30392
30392
  //#region ../../packages/agent-core/src/tools/builtin/state/todo-list.md?raw
30393
30393
  var todo_list_default;
30394
30394
  var init_todo_list$1 = __esmMin((() => {
30395
- todo_list_default = "Use this tool to maintain a structured TODO list as you work through a multi-step task. Use it proactively and often when progress tracking helps the current work. This is especially useful in long-running investigations and implementation tasks with several tool calls; in plan mode, write the plan to the plan file rather than tracking it here.\n\n**When to use:**\n- Multi-step tasks that span several tool calls\n- Tracking investigation progress across a large codebase search\n- Planning a sequence of edits before making them\n- After receiving new multi-step instructions, capture the requirements as todos\n- Before starting a tracked task, mark exactly one item as `in_progress`\n- Immediately after finishing a tracked task, mark it `done`; do not batch completions at the end\n\n**When NOT to use:**\n- Single-shot answers that complete in one or two tool calls\n- Trivial requests where tracking adds no clarity\n- Purely conversational or informational replies\n\n**Avoid churn:**\n- Do not re-call this tool when nothing meaningful has changed since the last call — update the list only after real progress.\n- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update.\n- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos.\n\n**How to use:**\n- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done.\n- Call with no `todos` argument to retrieve the current list without changing it.\n- Call with `todos: []` to clear the list.\n- Keep titles short and actionable (e.g. \"Read session-control.ts\", \"Add planMode flag to TurnManager\").\n- Update statuses as you make progress.\n- When work is underway, keep exactly one task `in_progress`.\n- Only mark a task `done` when it is fully accomplished.\n- Never mark a task `done` if tests are failing, implementation is partial, unresolved errors remain, or required files/dependencies could not be found.\n- If you encounter a blocker, keep the blocked task `in_progress` or add a new pending task describing what must be resolved.\n";
30395
+ todo_list_default = "Use this tool to maintain a structured TODO list as you work through a multi-step task. Use it proactively and often when progress tracking helps the current work. This is especially useful in long-running investigations and implementation tasks with several tool calls; in plan mode, write the plan to the plan file rather than tracking it here.\n\n**When to use:**\n- Multi-step tasks that span several tool calls\n- Tracking investigation progress across a large codebase search\n- Planning a sequence of edits before making them\n- After receiving new multi-step instructions, capture the requirements as todos\n- Before starting a tracked task, mark exactly one item as `in_progress`\n- Immediately after finishing a tracked task, record its truthful terminal state; do not batch completions at the end\n\n**When NOT to use:**\n- Single-shot answers that complete in one or two tool calls\n- Trivial requests where tracking adds no clarity\n- Purely conversational or informational replies\n\n**Avoid churn:**\n- Do not re-call this tool when nothing meaningful has changed since the last call — update the list only after real progress.\n- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update.\n- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos.\n\n**How to use:**\n- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done / blocked / waiting_approval / aborted.\n- Call with no `todos` argument to retrieve the current list without changing it.\n- Call with `todos: []` to clear the list.\n- Keep titles short and actionable (e.g. \"Read session-control.ts\", \"Add planMode flag to TurnManager\").\n- Update statuses as you make progress.\n- When work is underway, keep exactly one task `in_progress`.\n- Use `done` only when fully accomplished. Use `blocked` with the reason and the action that would unblock it, `waiting_approval` with the prepared result that awaits approval, and `aborted` with the last reached state.\n- Never mark a task `done` if tests are failing, implementation is partial, unresolved errors remain, or required files/dependencies could not be found.\n";
30396
30396
  }));
30397
30397
  //#endregion
30398
30398
  //#region ../../packages/agent-core/src/tools/builtin/state/todo-list.ts
@@ -30407,6 +30407,9 @@ function statusMarker(status) {
30407
30407
  case "pending": return "[pending]";
30408
30408
  case "in_progress": return "[in_progress]";
30409
30409
  case "done": return "[done]";
30410
+ case "blocked": return "[blocked]";
30411
+ case "waiting_approval": return "[waiting_approval]";
30412
+ case "aborted": return "[aborted]";
30410
30413
  default: return status;
30411
30414
  }
30412
30415
  }
@@ -30417,13 +30420,16 @@ var init_todo_list = __esmMin((() => {
30417
30420
  init_todo_list$1();
30418
30421
  TODO_LIST_TOOL_NAME = "TodoList";
30419
30422
  TODO_STORE_KEY = "todo";
30420
- TODO_LIST_WRITE_REMINDER = "Keep the task list aligned with the work. Mark finished items done promptly, keep one item in_progress while work is active, and clear items that no longer apply.";
30423
+ TODO_LIST_WRITE_REMINDER = "Keep the task list aligned with the work. Record a truthful terminal status for every finished item, keep one item in_progress while work is active, and clear items that no longer apply.";
30421
30424
  TodoItemSchema = object({
30422
30425
  title: string().min(1).describe("Short, actionable title for the todo."),
30423
30426
  status: _enum([
30424
30427
  "pending",
30425
30428
  "in_progress",
30426
- "done"
30429
+ "done",
30430
+ "blocked",
30431
+ "waiting_approval",
30432
+ "aborted"
30427
30433
  ]).describe("Current status of the todo.")
30428
30434
  });
30429
30435
  TodoListInputSchema = object({ todos: array(TodoItemSchema).optional().describe("The updated todo list. Omit to read the current todo list without making changes. Pass an empty array to clear the list.") });
@@ -76042,6 +76048,16 @@ function renderCronFireXml(origin, prompt) {
76042
76048
  "</cron-fire>"
76043
76049
  ].join("\n");
76044
76050
  }
76051
+ function renderSessionLoopWakeupXml(origin, prompt, interval) {
76052
+ return [
76053
+ `<session-loop-wakeup interval="${stringAttr(interval, "auto")}">`,
76054
+ "This is an autonomous scheduled wake-up, not a new user message.",
76055
+ "Resume responsibility for the recurring assignment below. Inspect the current state, research or act with the available tools, and make concrete progress independently.",
76056
+ "Do not ask the user what to do next unless a real decision, permission, or access blocker prevents progress. If no action is possible, report exactly what you checked and what condition you are waiting for.",
76057
+ "</session-loop-wakeup>",
76058
+ renderCronFireXml(origin, prompt)
76059
+ ].join("\n");
76060
+ }
76045
76061
  function stringAttr(value, fallback) {
76046
76062
  if (typeof value !== "string" || value.length === 0) return fallback;
76047
76063
  return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
@@ -77103,7 +77119,7 @@ var init_manager$3 = __esmMin((() => {
77103
77119
  };
77104
77120
  const content = [{
77105
77121
  type: "text",
77106
- text: renderCronFireXml(origin, task.prompt)
77122
+ text: task.owner === "session-loop" ? renderSessionLoopWakeupXml(origin, task.prompt, task.loopInterval ?? "auto") : renderCronFireXml(origin, task.prompt)
77107
77123
  }];
77108
77124
  this.agent.emitEvent({
77109
77125
  type: "cron.fired",
@@ -310917,31 +310933,36 @@ function mediaPayloadNumberArray(payload, keys) {
310917
310933
  }
310918
310934
  }
310919
310935
  function mediaProgressFromPayload(payload) {
310920
- const rawPercent = mediaPayloadNumber(payload, ["percent", "progress_percent", "progressPercent", "progress"]);
310936
+ const nestedProgress = payload["progress"];
310937
+ const source = typeof nestedProgress === "object" && nestedProgress !== null && !Array.isArray(nestedProgress) ? {
310938
+ ...payload,
310939
+ ...nestedProgress
310940
+ } : payload;
310941
+ const rawPercent = mediaPayloadNumber(source, ["percent", "progress_percent", "progressPercent", "progress"]);
310921
310942
  const percent = rawPercent === void 0 ? void 0 : rawPercent <= 1 ? rawPercent * 100 : rawPercent;
310922
310943
  const progress = {
310923
- phaseLabel: mediaPayloadText(payload, ["phase", "stage", "step_name", "stepName"]),
310944
+ phaseLabel: mediaPayloadText(source, ["phase", "stage", "step_name", "stepName"]),
310924
310945
  percent: percent === void 0 ? void 0 : Math.min(100, Math.max(0, percent)),
310925
- currentFrame: mediaPayloadNumber(payload, ["current_frame", "currentFrame", "frames_completed", "framesCompleted"]),
310926
- totalFrames: mediaPayloadNumber(payload, ["total_frames", "totalFrames", "frame_count", "frameCount"]),
310927
- fps: mediaPayloadNumber(payload, ["fps", "frames_per_second", "framesPerSecond"]),
310928
- currentStep: mediaPayloadNumber(payload, ["current_step", "currentStep", "step"]),
310929
- totalSteps: mediaPayloadNumber(payload, ["total_steps", "totalSteps", "steps"]),
310930
- currentSegment: mediaPayloadNumber(payload, ["current_segment", "currentSegment", "segment"]),
310931
- totalSegments: mediaPayloadNumber(payload, ["total_segments", "totalSegments", "segments"]),
310932
- generatedSeconds: mediaPayloadNumber(payload, ["generated_seconds", "generatedSeconds", "audio_seconds", "audioSeconds"]),
310933
- queuePosition: mediaPayloadNumber(payload, ["queue_position", "queuePosition"]),
310934
- elapsedSeconds: mediaPayloadNumber(payload, ["elapsed_seconds", "elapsedSeconds"]),
310935
- remainingSeconds: mediaPayloadNumber(payload, ["remaining_seconds", "remainingSeconds", "eta_seconds", "etaSeconds"]),
310936
- durationSeconds: mediaPayloadNumber(payload, ["duration_seconds", "durationSeconds"]),
310937
- width: mediaPayloadNumber(payload, ["width", "width_px", "widthPx"]),
310938
- height: mediaPayloadNumber(payload, ["height", "height_px", "heightPx"]),
310939
- previewUrl: mediaPayloadText(payload, ["preview_url", "previewUrl", "thumbnail_url", "thumbnailUrl"]),
310940
- previewDataBase64: mediaPayloadText(payload, ["preview_base64", "previewBase64", "thumbnail_base64", "thumbnailBase64"]),
310941
- previewMimeType: mediaPayloadText(payload, ["preview_mime_type", "previewMimeType", "thumbnail_mime_type", "thumbnailMimeType"]),
310942
- sampleUrl: mediaPayloadText(payload, ["sample_url", "sampleUrl", "audio_preview_url", "audioPreviewUrl"]),
310943
- localPath: mediaPayloadText(payload, ["local_path", "localPath"]),
310944
- waveform: mediaPayloadNumberArray(payload, ["waveform", "waveform_samples", "waveformSamples"])
310946
+ currentFrame: mediaPayloadNumber(source, ["current_frame", "currentFrame", "frames_completed", "framesCompleted"]),
310947
+ totalFrames: mediaPayloadNumber(source, ["total_frames", "totalFrames", "frame_count", "frameCount"]),
310948
+ fps: mediaPayloadNumber(source, ["fps", "frames_per_second", "framesPerSecond"]),
310949
+ currentStep: mediaPayloadNumber(source, ["current_step", "currentStep", "step"]),
310950
+ totalSteps: mediaPayloadNumber(source, ["total_steps", "totalSteps", "steps"]),
310951
+ currentSegment: mediaPayloadNumber(source, ["current_segment", "currentSegment", "segment"]),
310952
+ totalSegments: mediaPayloadNumber(source, ["total_segments", "totalSegments", "segments"]),
310953
+ generatedSeconds: mediaPayloadNumber(source, ["generated_seconds", "generatedSeconds", "audio_seconds", "audioSeconds"]),
310954
+ queuePosition: mediaPayloadNumber(source, ["queue_position", "queuePosition"]),
310955
+ elapsedSeconds: mediaPayloadNumber(source, ["elapsed_seconds", "elapsedSeconds"]),
310956
+ remainingSeconds: mediaPayloadNumber(source, ["remaining_seconds", "remainingSeconds", "eta_seconds", "etaSeconds"]),
310957
+ durationSeconds: mediaPayloadNumber(source, ["duration_seconds", "durationSeconds"]),
310958
+ width: mediaPayloadNumber(source, ["width", "width_px", "widthPx"]),
310959
+ height: mediaPayloadNumber(source, ["height", "height_px", "heightPx"]),
310960
+ previewUrl: mediaPayloadText(source, ["preview_url", "previewUrl", "thumbnail_url", "thumbnailUrl"]),
310961
+ previewDataBase64: mediaPayloadText(source, ["preview_base64", "previewBase64", "thumbnail_base64", "thumbnailBase64"]),
310962
+ previewMimeType: mediaPayloadText(source, ["preview_mime_type", "previewMimeType", "thumbnail_mime_type", "thumbnailMimeType"]),
310963
+ sampleUrl: mediaPayloadText(source, ["sample_url", "sampleUrl", "audio_preview_url", "audioPreviewUrl"]),
310964
+ localPath: mediaPayloadText(source, ["local_path", "localPath"]),
310965
+ waveform: mediaPayloadNumberArray(source, ["waveform", "waveform_samples", "waveformSamples"])
310945
310966
  };
310946
310967
  return Object.fromEntries(Object.entries(progress).filter(([, value]) => value !== void 0));
310947
310968
  }
@@ -334451,8 +334472,9 @@ function toolResultToSessionUpdate(sessionId, event) {
334451
334472
  * Mapping rules (anchored at types.gen.d.ts:3530-3569 / :4849):
334452
334473
  * - The `todo_list` input-display block carries
334453
334474
  * `items: { title, status }[]` (schemas.ts:60). The status is the
334454
- * three-state TodoStatus union (todo-list.ts:26):
334455
- * `pending` | `in_progress` | `done`.
334475
+ * TodoStatus union (todo-list.ts:26):
334476
+ * `pending` | `in_progress` | `done` | `blocked` |
334477
+ * `waiting_approval` | `aborted`.
334456
334478
  * - ACP {@link PlanEntryStatus} is `pending` | `in_progress` | `completed`,
334457
334479
  * so `done` rewrites to `completed`. Anything outside the known
334458
334480
  * enum lands on `pending` as a safe default — we never want a
@@ -334486,6 +334508,9 @@ function mapTodoStatus(status) {
334486
334508
  case "in_progress": return "in_progress";
334487
334509
  case "done":
334488
334510
  case "completed": return "completed";
334511
+ case "blocked":
334512
+ case "waiting_approval":
334513
+ case "aborted": return "pending";
334489
334514
  default: return "pending";
334490
334515
  }
334491
334516
  }
@@ -401361,6 +401386,21 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
401361
401386
  return "idle-only";
401362
401387
  }
401363
401388
  },
401389
+ {
401390
+ name: "idea",
401391
+ aliases: [],
401392
+ descriptionKey: "command.goal.description",
401393
+ priority: 80,
401394
+ argumentHint: "<objective>",
401395
+ availability: (args) => {
401396
+ const trimmed = args.trim();
401397
+ if (trimmed === "") return "always";
401398
+ const tokens = trimmed.split(/\s+/);
401399
+ const action = tokens[0]?.toLowerCase();
401400
+ if (tokens.length === 1 && (action === "status" || action === "pause" || action === "paused" || action === "resume" || action === "cancel" || action === "clear" || action === "stop")) return "always";
401401
+ return "idle-only";
401402
+ }
401403
+ },
401364
401404
  {
401365
401405
  name: "loop",
401366
401406
  aliases: [],
@@ -411873,7 +411913,7 @@ function isTodoItemShape(value) {
411873
411913
  if (typeof value !== "object" || value === null) return false;
411874
411914
  const rec = value;
411875
411915
  if (typeof rec.title !== "string" || rec.title.length === 0) return false;
411876
- return rec.status === "pending" || rec.status === "in_progress" || rec.status === "done";
411916
+ return rec.status === "pending" || rec.status === "in_progress" || rec.status === "done" || rec.status === "blocked" || rec.status === "waiting_approval" || rec.status === "aborted";
411877
411917
  }
411878
411918
  const PUBLIC_IDENTITY_ASSIGNMENT_RE = /\b(provider|model)(["']?\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^,;\s)\]}]+)/giu;
411879
411919
  /** Project structured runtime identity fields onto the public BLUN identity. */
@@ -412659,6 +412699,7 @@ var ChoicePickerComponent = class extends Container {
412659
412699
  const descriptionColor = isSelected && opt.descriptionTone !== void 0 ? opt.descriptionTone : "textMuted";
412660
412700
  for (const descLine of wrapDescription$1(opt.description, descriptionWidth)) lines.push(currentTheme.fg(descriptionColor, ` ${descLine}`));
412661
412701
  }
412702
+ if (this.opts.spaced === true && i < view.page.end - 1) lines.push("");
412662
412703
  }
412663
412704
  lines.push("");
412664
412705
  if (view.page.pageCount > 1) lines.push(currentTheme.fg("textMuted", ` ${uiText("common.pageCount", {
@@ -416649,6 +416690,19 @@ function bufferContext(state, chatId, tag) {
416649
416690
  function channelOrigin(envelope) {
416650
416691
  return `Telegram · ${envelope.meta.user ?? envelope.meta.chat_id}`;
416651
416692
  }
416693
+ const TELEGRAM_REMOTE_COMMANDS = Object.freeze(["loop", "goal", "idea", "befehle"]);
416694
+ function telegramRemoteCommand(text) {
416695
+ const trimmed = text.trim();
416696
+ const parsed = parseSlashInput(trimmed);
416697
+ if (parsed === null) return;
416698
+ const name = parsed.name.toLowerCase().split("@", 1)[0];
416699
+ if (!TELEGRAM_REMOTE_COMMANDS.includes(name)) return;
416700
+ return {
416701
+ name,
416702
+ input: name === "befehle" ? "/befehle" : `/${name}${parsed.args.length > 0 ? ` ${parsed.args}` : ""}`,
416703
+ displayText: trimmed
416704
+ };
416705
+ }
416652
416706
  /**
416653
416707
  * Route one envelope into the visible session. Unaddressed group traffic is
416654
416708
  * shown and buffered per chat without a model turn. Addressed traffic queues
@@ -420076,6 +420130,51 @@ async function handleGoalCommand(host, args) {
420076
420130
  return;
420077
420131
  }
420078
420132
  }
420133
+ const IDEA_ALLOWED_CHANNELS = Object.freeze([]);
420134
+ const IDEA_TERMINAL_TODO_STATUSES = Object.freeze([
420135
+ "done",
420136
+ "blocked",
420137
+ "waiting_approval",
420138
+ "aborted"
420139
+ ]);
420140
+ function buildIdeaPrompt(objective) {
420141
+ const allowedChannels = IDEA_ALLOWED_CHANNELS.length === 0 ? "none" : IDEA_ALLOWED_CHANNELS.join(", ");
420142
+ return `<idea-mode>
420143
+ The user's objective is:
420144
+ ${objective}
420145
+
420146
+ Your first tool call MUST be TodoList. Publish a task-specific plan there before any research or action.
420147
+
420148
+ ${buildIdeaContract(allowedChannels)}
420149
+ </idea-mode>`;
420150
+ }
420151
+ function buildIdeaContract(allowedChannels = IDEA_ALLOWED_CHANNELS.length === 0 ? "none" : IDEA_ALLOWED_CHANNELS.join(", ")) {
420152
+ return `Work as a self-directing employee: discover a useful route, research current examples and real user problems when relevant, decide, implement reversible local work, verify results, and keep going without waiting for step-by-step instructions.
420153
+
420154
+ Maintain the task-specific plan in TodoList. Keep exactly one item in_progress. Every plan item must end in exactly one truthful terminal status from: ${IDEA_TERMINAL_TODO_STATUSES.join(", ")}. A blocked title must include the reason and what would unblock it. A waiting_approval title must include the prepared result and the exact approval needed. An aborted title must include the last reached state.
420155
+
420156
+ Ask a focused question with AskUserQuestion only when the answer changes the next action or when access is missing. Otherwise act.
420157
+
420158
+ Read-only research through search and fetch tools is allowed. External publishing, messages to third parties, account creation, purchases, or other irreversible external actions are allowed only for an exact channel listed in IDEA_ALLOWED_CHANNELS and only with the user's own required access. Currently allowed channels: ${allowedChannels}. If a later step would leave the computer and is not preapproved, prepare everything possible, mark the step waiting_approval, and ask for the precise approval or access. Never skip the step silently.
420159
+
420160
+ Do not stop after proposing ideas. Carry safe local work through implementation and verification. The assignment is complete only when every plan item has a truthful terminal status and every claimed result has been verified. Report concrete results, blockers, or decisions.`;
420161
+ }
420162
+ async function handleIdeaCommand(host, args) {
420163
+ const parsed = parseGoalCommand(args);
420164
+ if (parsed.kind !== "create") {
420165
+ await handleGoalCommand(host, args);
420166
+ return;
420167
+ }
420168
+ await createGoal(host, {
420169
+ ...parsed,
420170
+ completionCriterion: buildIdeaContract()
420171
+ }, args, {
420172
+ commandName: "idea",
420173
+ sendInput: (objective) => {
420174
+ host.sendMessage(host.requireSession(), objective, { parts: buildIdeaPrompt(objective) });
420175
+ }
420176
+ });
420177
+ }
420079
420178
  function parseNextGoalCommand(tokens) {
420080
420179
  if (tokens.length === 2 && tokens[1]?.toLowerCase() === "manage") return { kind: "next-manage" };
420081
420180
  let index = 1;
@@ -420218,7 +420317,7 @@ async function createGoal(host, parsed, rawArgs, options = {}) {
420218
420317
  return startGoal(host, parsed, options);
420219
420318
  }
420220
420319
  function showGoalStartPermissionPrompt(host, parsed, rawArgs, options) {
420221
- const commandText = `/goal ${rawArgs.trim()}`;
420320
+ const commandText = `/${options.commandName ?? "goal"} ${rawArgs.trim()}`;
420222
420321
  const cancelStart = () => {
420223
420322
  host.restoreInputText(commandText);
420224
420323
  host.showStatus(uiText("goal.start.notStarted"));
@@ -420255,6 +420354,7 @@ async function startGoal(host, parsed, options) {
420255
420354
  try {
420256
420355
  await host.requireSession().createGoal({
420257
420356
  objective: parsed.objective,
420357
+ completionCriterion: parsed.completionCriterion,
420258
420358
  replace: parsed.replace
420259
420359
  });
420260
420360
  } catch (error) {
@@ -420329,6 +420429,11 @@ async function showGoalStatus(host) {
420329
420429
  host.showStatus(uiText("goal.status.noneSet"));
420330
420430
  return;
420331
420431
  }
420432
+ const telegramCommand = host.telegramRemoteCommandContext?.item.telegramCommandName;
420433
+ if (telegramCommand === "goal" || telegramCommand === "idea") {
420434
+ host.showStatus(`/${telegramCommand} ${goal.status} · ${goal.objective}`);
420435
+ return;
420436
+ }
420332
420437
  host.state.transcriptContainer.addChild(new GoalStatusMessageComponent(goal));
420333
420438
  host.state.ui.requestRender();
420334
420439
  }
@@ -420480,6 +420585,7 @@ async function handleLoopCommand(host, args) {
420480
420585
  switch (parsed.kind) {
420481
420586
  case "status": {
420482
420587
  const { loop } = await session.getLoop();
420588
+ host.setAppState?.({ loop });
420483
420589
  host.track("loop_status", { status: loop?.status ?? "none" });
420484
420590
  host.showStatus(`/loop ${loop?.status ?? "none"}`);
420485
420591
  return;
@@ -420489,6 +420595,7 @@ async function handleLoopCommand(host, args) {
420489
420595
  interval: parsed.interval,
420490
420596
  prompt: parsed.prompt
420491
420597
  });
420598
+ host.setAppState?.({ loop });
420492
420599
  host.track("loop_start", { interval: loop.interval });
420493
420600
  host.showStatus(`/loop ${loop.status}`);
420494
420601
  host.sendNormalUserInput(loop.prompt);
@@ -420496,6 +420603,7 @@ async function handleLoopCommand(host, args) {
420496
420603
  }
420497
420604
  case "pause": {
420498
420605
  const loop = await session.pauseLoop();
420606
+ host.setAppState?.({ loop });
420499
420607
  if (isStreaming(host)) await session.cancel();
420500
420608
  host.track("loop_pause");
420501
420609
  host.showStatus(`/loop ${loop.status}`);
@@ -420503,12 +420611,14 @@ async function handleLoopCommand(host, args) {
420503
420611
  }
420504
420612
  case "resume": {
420505
420613
  const loop = await session.resumeLoop();
420614
+ host.setAppState?.({ loop });
420506
420615
  host.track("loop_resume");
420507
420616
  host.showStatus(`/loop ${loop.status}`);
420508
420617
  return;
420509
420618
  }
420510
420619
  case "stop":
420511
420620
  await session.stopLoop();
420621
+ host.setAppState?.({ loop: null });
420512
420622
  if (isStreaming(host)) await session.cancel();
420513
420623
  host.track("loop_stop");
420514
420624
  host.showStatus("/loop stopped");
@@ -493079,6 +493189,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
493079
493189
  case "goal":
493080
493190
  await handleGoalCommand(host, args);
493081
493191
  return;
493192
+ case "idea":
493193
+ await handleIdeaCommand(host, args);
493194
+ return;
493082
493195
  case "loop":
493083
493196
  await handleLoopCommand(host, args);
493084
493197
  return;
@@ -498256,23 +498369,19 @@ function mediaActivityCanonicalPhase(value) {
498256
498369
  }
498257
498370
  function mediaActivityPhaseSequence(mediaKind) {
498258
498371
  switch (mediaActivityStatusKey(mediaKind)) {
498259
- case "image": return ["queued", "generating", "upscaling", "saving"];
498260
- case "video": return ["queued", "generating", "rendering", "saving"];
498372
+ case "image": return ["queued", "processing", "generating", "upscaling", "saving"];
498373
+ case "video": return ["queued", "processing", "generating", "rendering", "saving"];
498261
498374
  case "voice": return ["queued", "processing", "saving"];
498262
- case "dubbing": return ["queued", "audio-analysis", "rendering", "saving"];
498263
- case "lipsync": return ["queued", "audio-analysis", "face-tracking", "lip-sync-running", "rendering", "exporting", "saving"];
498375
+ case "dubbing": return ["queued", "processing", "audio-analysis", "rendering", "saving"];
498376
+ case "lipsync": return ["queued", "processing", "audio-analysis", "face-tracking", "lip-sync-running", "rendering", "exporting", "saving"];
498264
498377
  case "image-analysis":
498265
- case "video-analysis": return ["queued", "analyzing"];
498378
+ case "video-analysis": return ["queued", "processing", "analyzing"];
498266
498379
  default: return ["queued", "processing", "saving"];
498267
498380
  }
498268
498381
  }
498269
498382
  function mediaActivityChainStates(job) {
498270
498383
  const sequence = mediaActivityPhaseSequence(job.mediaKind);
498271
- const normalize = (value) => {
498272
- const phase = mediaActivityCanonicalPhase(value);
498273
- if (phase === "processing" && !sequence.includes(phase)) return sequence[1] ?? phase;
498274
- return phase;
498275
- };
498384
+ const normalize = (value) => mediaActivityCanonicalPhase(value);
498276
498385
  const current = normalize(job.phaseLabel ?? job.phase);
498277
498386
  const currentIndex = sequence.indexOf(current);
498278
498387
  const history = new Set((job.phaseHistory ?? []).map(normalize));
@@ -498437,7 +498546,7 @@ var MediaActivityComponent = class {
498437
498546
  }
498438
498547
  chainLine(job, width) {
498439
498548
  const chain = mediaActivityChainStates(job).map(({ phase, state }) => {
498440
- const symbol = state === "done" ? "[x]" : state === "active" ? "[>]" : "[ ]";
498549
+ const symbol = state === "done" ? "" : state === "active" ? "" : "";
498441
498550
  const label = mediaActivityLabel("media.phase", phase, phase);
498442
498551
  const text = `${symbol} ${label}`;
498443
498552
  if (state === "done") return currentTheme.fg("success", text);
@@ -498447,13 +498556,27 @@ var MediaActivityComponent = class {
498447
498556
  return truncateToWidth(` ${currentTheme.fg("textDim", mediaUiText("media.activity.chain"))} · ${chain.join(currentTheme.fg("textDim", " · "))}`, Math.max(1, width), "…");
498448
498557
  }
498449
498558
  previewImage(job) {
498450
- if (typeof job.previewDataBase64 !== "string" || job.previewDataBase64.length === 0 || job.previewDataBase64.length > 2 * 1024 * 1024 || !/^[A-Za-z0-9+/=\r\n]+$/u.test(job.previewDataBase64)) return;
498451
- const mimeType = typeof job.previewMimeType === "string" ? job.previewMimeType.toLowerCase() : "";
498559
+ let previewDataBase64 = typeof job.previewDataBase64 === "string" ? job.previewDataBase64 : "";
498560
+ let mimeType = typeof job.previewMimeType === "string" ? job.previewMimeType.toLowerCase() : "";
498561
+ if (previewDataBase64.length === 0 && typeof job.previewUrl === "string") {
498562
+ const dataUrl = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=\r\n]+)$/iu.exec(job.previewUrl.trim());
498563
+ if (dataUrl !== null) {
498564
+ mimeType = dataUrl[1].toLowerCase();
498565
+ previewDataBase64 = dataUrl[2];
498566
+ }
498567
+ }
498568
+ if (previewDataBase64.length === 0 || previewDataBase64.length > 2 * 1024 * 1024 || !/^[A-Za-z0-9+/=\r\n]+$/u.test(previewDataBase64)) return;
498452
498569
  if (!["image/png", "image/jpeg", "image/webp", "image/gif"].includes(mimeType)) return;
498453
- const key = `${job.id ?? job.toolCallId}:${mimeType}:${job.previewDataBase64.length}`;
498570
+ const identity = String(job.id ?? job.toolCallId);
498571
+ const prefix = `${identity}:`;
498572
+ const key = `${prefix}${mimeType}:${String(job.observedAt ?? job.updatedAt ?? previewDataBase64.length)}`;
498454
498573
  let image = this.previewImages.get(key);
498455
498574
  if (image === void 0) {
498456
- image = new Image(job.previewDataBase64, mimeType, { fallbackColor: (text) => currentTheme.fg("textDim", text) }, {
498575
+ for (const [previousKey, previousImage] of this.previewImages) if (previousKey.startsWith(prefix)) {
498576
+ previousImage.invalidate();
498577
+ this.previewImages.delete(previousKey);
498578
+ }
498579
+ image = new Image(previewDataBase64, mimeType, { fallbackColor: (text) => currentTheme.fg("textDim", text) }, {
498457
498580
  maxHeightCells: 5,
498458
498581
  maxWidthCells: 24,
498459
498582
  filename: mediaUiText("media.detail.preview")
@@ -504837,6 +504960,9 @@ var SessionEventHandler = class {
504837
504960
  stale: event.origin.stale
504838
504961
  }
504839
504962
  });
504963
+ setTimeout(() => {
504964
+ this.host.refreshLoopState();
504965
+ }, 0);
504840
504966
  }
504841
504967
  handleTurnEnd(event, sendQueued) {
504842
504968
  this.turnWatchdog.stop();
@@ -507285,6 +507411,30 @@ function formatGoalBadge(goal, colors, wallClockMs) {
507285
507411
  const label = `${uiText(`goal.panel.status.${goal.status}`)} · ${formatBadgeElapsed(wallClockMs ?? goal.wallClockMs)} · ${turns}`;
507286
507412
  return chalk.hex(colors.textMuted)(`[${uiText("footer.goal.label")} `) + chalk.hex(dotColor)("●") + chalk.hex(colors.textMuted)(` ${label}]`);
507287
507413
  }
507414
+ function formatLoopBadge(loop, colors, nowMs = Date.now()) {
507415
+ if (loop === null || loop === void 0 || loop.status !== "active" || !Number.isFinite(loop.nextFireAt)) return null;
507416
+ const remainingMs = Math.max(0, loop.nextFireAt - nowMs);
507417
+ let value;
507418
+ let unit;
507419
+ if (remainingMs < 9e4) {
507420
+ value = Math.ceil(remainingMs / 1e3);
507421
+ unit = "second";
507422
+ } else if (remainingMs < 54e5) {
507423
+ value = Math.ceil(remainingMs / 6e4);
507424
+ unit = "minute";
507425
+ } else if (remainingMs < 1296e5) {
507426
+ value = Math.ceil(remainingMs / 36e5);
507427
+ unit = "hour";
507428
+ } else {
507429
+ value = Math.ceil(remainingMs / 864e5);
507430
+ unit = "day";
507431
+ }
507432
+ const relative = new Intl.RelativeTimeFormat(getCurrentUiLocale(), {
507433
+ numeric: "always",
507434
+ style: "short"
507435
+ }).format(value, unit);
507436
+ return chalk.hex(colors.textMuted)(`[/loop · ${relative}]`);
507437
+ }
507288
507438
  function formatBadgeElapsed(ms) {
507289
507439
  const totalSeconds = Math.round(ms / 1e3);
507290
507440
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -507463,6 +507613,7 @@ var FooterComponent = class {
507463
507613
  goalSnapshotKey = null;
507464
507614
  goalObservedAtMs = Date.now();
507465
507615
  goalTimer = null;
507616
+ loopTimer = null;
507466
507617
  compactionAttemptStartedAtMs = null;
507467
507618
  compactionEstimatedInputTokens;
507468
507619
  compactionProgress;
@@ -507484,6 +507635,7 @@ var FooterComponent = class {
507484
507635
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onRefresh });
507485
507636
  this.syncGoalClock(state.goal);
507486
507637
  this.syncGoalTimer(state.goal);
507638
+ this.syncLoopTimer(state.loop);
507487
507639
  if (state.isCompacting) this.startCompaction();
507488
507640
  }
507489
507641
  setState(state) {
@@ -507493,6 +507645,7 @@ var FooterComponent = class {
507493
507645
  }
507494
507646
  this.syncGoalClock(state.goal);
507495
507647
  this.syncGoalTimer(state.goal);
507648
+ this.syncLoopTimer(state.loop);
507496
507649
  this.state = state;
507497
507650
  if (state.isCompacting && this.compactionAttemptStartedAtMs === null) this.startCompaction();
507498
507651
  else if (!state.isCompacting) this.finishCompaction();
@@ -507559,6 +507712,8 @@ var FooterComponent = class {
507559
507712
  if (modes.length > 0) left.push(modes.join(" "));
507560
507713
  const goalBadge = formatGoalBadge(state.goal, colors, this.goalWallClockMs(state.goal));
507561
507714
  if (goalBadge !== null) left.push(goalBadge);
507715
+ const loopBadge = formatLoopBadge(state.loop, colors);
507716
+ if (loopBadge !== null) left.push(loopBadge);
507562
507717
  const model = modelDisplayName(state);
507563
507718
  if (model) {
507564
507719
  const sep = chalk.hex(colors.textMuted)(" · ");
@@ -507623,11 +507778,29 @@ var FooterComponent = class {
507623
507778
  this.goalTimer = null;
507624
507779
  }
507625
507780
  }
507781
+ syncLoopTimer(loop) {
507782
+ if (loop?.status === "active" && Number.isFinite(loop.nextFireAt)) {
507783
+ if (this.loopTimer !== null) return;
507784
+ this.loopTimer = setInterval(() => {
507785
+ this.onRefresh();
507786
+ }, GOAL_TIMER_INTERVAL_MS);
507787
+ this.loopTimer.unref?.();
507788
+ return;
507789
+ }
507790
+ if (this.loopTimer !== null) {
507791
+ clearInterval(this.loopTimer);
507792
+ this.loopTimer = null;
507793
+ }
507794
+ }
507626
507795
  dispose() {
507627
507796
  if (this.goalTimer !== null) {
507628
507797
  clearInterval(this.goalTimer);
507629
507798
  this.goalTimer = null;
507630
507799
  }
507800
+ if (this.loopTimer !== null) {
507801
+ clearInterval(this.loopTimer);
507802
+ this.loopTimer = null;
507803
+ }
507631
507804
  this.finishCompaction();
507632
507805
  }
507633
507806
  goalWallClockMs(goal) {
@@ -510396,6 +510569,8 @@ const MAX_VISIBLE = 5;
510396
510569
  const BLUN_BLUE = "#0261ff";
510397
510570
  const BLUN_BRIGHT = "#4f8dff";
510398
510571
  const DONE_GREEN = "#22c55e";
510572
+ const BLOCKED_RED = "#ef4444";
510573
+ const WAITING_AMBER = "#f59e0b";
510399
510574
  const BOX_EMPTY = "#2a3550";
510400
510575
  const PENDING_TEXT = "#aeb8c6";
510401
510576
  const LEFT_BAR = "▎";
@@ -510409,7 +510584,9 @@ const LEFT_BAR = "▎";
510409
510584
  *
510410
510585
  * Strategy:
510411
510586
  * 1. Include every `in_progress` item (capped at MAX_VISIBLE).
510412
- * 2. Fill remaining slots with "what's next" — the earliest `pending`
510587
+ * 2. Include terminal attention states (`blocked`, `waiting_approval`,
510588
+ * `aborted`) before ordinary pending and done work.
510589
+ * 3. Fill remaining slots with "what's next" — the earliest `pending`
510413
510590
  * items in their original positions — while reserving one slot for
510414
510591
  * "what just finished" — the latest `done` item — when both kinds
510415
510592
  * exist. If one side has too few candidates, the other expands.
@@ -510423,17 +510600,26 @@ function selectVisibleTodos(todos) {
510423
510600
  hiddenCounts: {
510424
510601
  done: 0,
510425
510602
  in_progress: 0,
510426
- pending: 0
510603
+ pending: 0,
510604
+ blocked: 0,
510605
+ waiting_approval: 0,
510606
+ aborted: 0
510427
510607
  }
510428
510608
  };
510429
510609
  const inProgress = [];
510430
510610
  const pending = [];
510431
510611
  const done = [];
510612
+ const attention = [];
510432
510613
  for (const [i, todo] of todos.entries()) if (todo.status === "in_progress") inProgress.push(i);
510433
510614
  else if (todo.status === "pending") pending.push(i);
510434
- else done.push(i);
510615
+ else if (todo.status === "done") done.push(i);
510616
+ else attention.push(i);
510435
510617
  const picked = /* @__PURE__ */ new Set();
510436
510618
  for (const i of inProgress.slice(0, MAX_VISIBLE)) picked.add(i);
510619
+ for (const i of attention) {
510620
+ if (picked.size >= MAX_VISIBLE) break;
510621
+ picked.add(i);
510622
+ }
510437
510623
  if (picked.size < MAX_VISIBLE) {
510438
510624
  const doneCandidates = done.toReversed();
510439
510625
  const pendingCandidates = pending;
@@ -510458,7 +510644,10 @@ function selectVisibleTodos(todos) {
510458
510644
  const hiddenCounts = {
510459
510645
  done: 0,
510460
510646
  in_progress: 0,
510461
- pending: 0
510647
+ pending: 0,
510648
+ blocked: 0,
510649
+ waiting_approval: 0,
510650
+ aborted: 0
510462
510651
  };
510463
510652
  for (const [i, todo] of todos.entries()) if (!picked.has(i)) hiddenCounts[todo.status] += 1;
510464
510653
  return {
@@ -510535,6 +510724,9 @@ function renderRow(todo, width) {
510535
510724
  return truncateToWidth(leftPart + chalk.hex("#ffffff")(title) + " ".repeat(gap) + chalk.hex(currentTheme.palette.textDim)(runningLabel), width);
510536
510725
  }
510537
510726
  if (todo.status === "done") return " " + chalk.hex(DONE_GREEN)("▪") + " " + chalk.hex(currentTheme.palette.textDim).strikethrough(todo.title);
510727
+ if (todo.status === "blocked") return " " + chalk.hex(BLOCKED_RED)("■") + " " + chalk.hex("#fecaca")(todo.title);
510728
+ if (todo.status === "waiting_approval") return " " + chalk.hex(WAITING_AMBER)("◆") + " " + chalk.hex("#fde68a")(todo.title);
510729
+ if (todo.status === "aborted") return " " + chalk.hex(currentTheme.palette.textDim)("×") + " " + chalk.hex(currentTheme.palette.textDim).strikethrough(todo.title);
510538
510730
  return " " + chalk.hex(BOX_EMPTY)("☐") + " " + chalk.hex(PENDING_TEXT)(todo.title);
510539
510731
  }
510540
510732
  //#endregion
@@ -511489,6 +511681,22 @@ function outboxGrewForChat(marker, chatId) {
511489
511681
  } catch {}
511490
511682
  return false;
511491
511683
  }
511684
+ function outboxDeliveredFile(marker, chatId, filePath) {
511685
+ try {
511686
+ if (statSync(outboxPath()).size <= marker) return false;
511687
+ const fresh = readFileSync(outboxPath()).subarray(marker).toString("utf8");
511688
+ const wanted = resolve(filePath).toLowerCase();
511689
+ for (const line of fresh.split(/\r?\n/)) {
511690
+ if (line.trim().length === 0) continue;
511691
+ try {
511692
+ const entry = JSON.parse(line);
511693
+ if (String(entry.chat_id) !== String(chatId) || !Array.isArray(entry.files)) continue;
511694
+ if (entry.files.some((file) => typeof file === "string" && resolve(file).toLowerCase() === wanted)) return true;
511695
+ } catch {}
511696
+ }
511697
+ } catch {}
511698
+ return false;
511699
+ }
511492
511700
  const TELEGRAM_TEXT_LIMIT = 4096;
511493
511701
  const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
511494
511702
  function mediaTelegramTarget(filePath) {
@@ -512216,19 +512424,20 @@ function promptStartupWorkspaceChoice(host, currentPath, copy) {
512216
512424
  };
512217
512425
  host.mountEditorReplacement(new ChoicePickerComponent({
512218
512426
  title: copy.choiceTitle,
512427
+ spaced: true,
512219
512428
  options: [
512220
512429
  {
512221
512430
  value: "use",
512222
- label: copy.useCurrent,
512431
+ label: `1. ${copy.useCurrent}`,
512223
512432
  description: currentPath
512224
512433
  },
512225
512434
  {
512226
512435
  value: "change",
512227
- label: copy.chooseOther
512436
+ label: `2. ${copy.chooseOther}`
512228
512437
  },
512229
512438
  {
512230
512439
  value: "exit",
512231
- label: copy.exit,
512440
+ label: `3. ${copy.exit}`,
512232
512441
  tone: "danger"
512233
512442
  }
512234
512443
  ],
@@ -512642,6 +512851,7 @@ function createInitialAppState(input) {
512642
512851
  availableProviders: {},
512643
512852
  sessionTitle: null,
512644
512853
  goal: null,
512854
+ loop: null,
512645
512855
  mcpServersSummary: null,
512646
512856
  banner: void 0
512647
512857
  };
@@ -512683,6 +512893,7 @@ var BlunTUI = class {
512683
512893
  startupLoginRequired = false;
512684
512894
  startupWorkspaceSelectionPending = false;
512685
512895
  startupGoalPromptedSessionId;
512896
+ startupPhaseMs = {};
512686
512897
  lastActivityMode;
512687
512898
  currentLoadingTip = void 0;
512688
512899
  mediaActivityStore = new MediaActivityStore();
@@ -512716,6 +512927,17 @@ var BlunTUI = class {
512716
512927
  track(event, properties) {
512717
512928
  this.harness.track(event, properties);
512718
512929
  }
512930
+ async measureStartupPhase(name, action) {
512931
+ const startedAt = Date.now();
512932
+ try {
512933
+ return await action();
512934
+ } finally {
512935
+ this.startupPhaseMs[name] = Date.now() - startedAt;
512936
+ }
512937
+ }
512938
+ getStartupPhaseMs() {
512939
+ return { ...this.startupPhaseMs };
512940
+ }
512719
512941
  constructor(harness, startupInput) {
512720
512942
  this.harness = harness;
512721
512943
  const initialAppState = createInitialAppState(startupInput);
@@ -512865,7 +513087,7 @@ var BlunTUI = class {
512865
513087
  }
512866
513088
  async start() {
512867
513089
  this.registerSignalHandlers();
512868
- this.startupWorkspaceSelectionPending = true;
513090
+ this.startupWorkspaceSelectionPending = !(this.options.startup.sessionFlag !== void 0 || this.options.startup.continueLast);
512869
513091
  try {
512870
513092
  const shouldReplayHistory = await this.initMainTui();
512871
513093
  this.startEventLoop();
@@ -513077,9 +513299,12 @@ var BlunTUI = class {
513077
513299
  this.showStatus(warning, "warning");
513078
513300
  }
513079
513301
  async init() {
513080
- setExperimentalFeatures(await this.harness.getExperimentalFeatures());
513081
- await this.authFlow.refreshAvailableModels();
513082
- const accountContext = await this.authFlow.refreshManagedAccountContextResult();
513302
+ if (this.startupWorkspaceSelectionPending) return false;
513303
+ await this.measureStartupPhase("experimental_ms", async () => {
513304
+ setExperimentalFeatures(await this.harness.getExperimentalFeatures());
513305
+ });
513306
+ await this.measureStartupPhase("models_ms", () => this.authFlow.refreshAvailableModels());
513307
+ const accountContext = await this.measureStartupPhase("account_ms", () => this.authFlow.refreshManagedAccountContextResult());
513083
513308
  if (accountContext.kind !== "ready") {
513084
513309
  if (accountContext.kind === "unauthenticated") {
513085
513310
  this.authFlow.enterLoginRequiredStartupState();
@@ -513087,8 +513312,7 @@ var BlunTUI = class {
513087
513312
  } else this.authFlow.enterManagedAccountUnavailableStartupState(accountContext.kind);
513088
513313
  return false;
513089
513314
  }
513090
- if (this.startupWorkspaceSelectionPending) return false;
513091
- await this.refreshProviderModelsBeforeSession();
513315
+ await this.measureStartupPhase("provider_models_ms", () => this.refreshProviderModelsBeforeSession());
513092
513316
  const { startup } = this.options;
513093
513317
  const { workDir } = this.state.appState;
513094
513318
  let session;
@@ -513103,6 +513327,7 @@ var BlunTUI = class {
513103
513327
  const channelMcpServers = sessionMcpServers();
513104
513328
  if (channelMcpServers !== void 0) createSessionOptions.mcpServers = channelMcpServers;
513105
513329
  if (this.state.appState.additionalDirs.length > 0) createSessionOptions.additionalDirs = [...this.state.appState.additionalDirs];
513330
+ const sessionStartedAt = Date.now();
513106
513331
  try {
513107
513332
  if (isResumeStartup) {
513108
513333
  if (startup.sessionFlag === "") {
@@ -513148,10 +513373,12 @@ var BlunTUI = class {
513148
513373
  this.authFlow.enterLoginRequiredStartupState();
513149
513374
  this.startupLoginRequired = true;
513150
513375
  return false;
513376
+ } finally {
513377
+ this.startupPhaseMs["session_open_ms"] = Date.now() - sessionStartedAt;
513151
513378
  }
513152
513379
  if (session === void 0) throw new Error(uiText("blunTui.startup.sessionNotInitialized"));
513153
- await this.setSession(session);
513154
- await this.syncRuntimeState(session);
513380
+ await this.measureStartupPhase("session_bind_ms", () => this.setSession(session, { refreshPersonalMemory: false }));
513381
+ await this.measureStartupPhase("runtime_state_ms", () => this.syncRuntimeState(session));
513155
513382
  this.applyStartupPermissionAndPlanToAppState();
513156
513383
  this.state.startupState = "ready";
513157
513384
  return shouldReplayHistory;
@@ -513434,6 +513661,7 @@ var BlunTUI = class {
513434
513661
  }
513435
513662
  queueDrainTimer;
513436
513663
  queueCommandRunning = false;
513664
+ telegramRemoteCommandContext;
513437
513665
  queueFlushBatchRemaining = 0;
513438
513666
  queueSteerInFlight;
513439
513667
  editorReplacementActive = false;
@@ -513684,14 +513912,20 @@ var BlunTUI = class {
513684
513912
  }
513685
513913
  /**
513686
513914
  * Remote channel inbound (Telegram) into THIS visible session.
513687
- * Never routes through handleUserInput: no slash/bash dispatch (a Telegram
513688
- * "/new" is just text for the model), no local editor history. The transcript
513915
+ * Only the explicit Telegram allowlist routes through slash dispatch; every
513916
+ * other slash-looking message (including "/new") remains plain model text.
513917
+ * There is no bash dispatch and no local editor history. The transcript
513689
513918
  * shows a clean user line with a muted origin prefix; the
513690
513919
  * model receives the full <channel …> payload. Busy turns queue via the same
513691
513920
  * queuedMessages mechanic as typed input; a completed tool/step boundary
513692
513921
  * steers one FIFO head into the active turn without interrupting it.
513693
513922
  */
513694
513923
  injectChannelMessage(envelope, acknowledge) {
513924
+ const remoteCommand = channelMessageAddressed(envelope) ? telegramRemoteCommand(envelope.text) : void 0;
513925
+ if (remoteCommand !== void 0) {
513926
+ this.injectTelegramRemoteCommand(envelope, remoteCommand, acknowledge);
513927
+ return;
513928
+ }
513695
513929
  injectChannelEnvelope({
513696
513930
  canDeliver: () => this.session !== void 0 && this.state.appState.model.trim().length > 0,
513697
513931
  isBusy: () => this.state.queuedMessages.length > 0 || this.queueSteerInFlight !== void 0 || this.deferUserMessages || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting,
@@ -513732,6 +513966,99 @@ var BlunTUI = class {
513732
513966
  }
513733
513967
  }, envelope, this.channelPreamble);
513734
513968
  }
513969
+ injectTelegramRemoteCommand(envelope, command, acknowledge) {
513970
+ if (this.session === void 0 || this.state.appState.model.trim().length === 0) {
513971
+ this.showStatus(uiText("channelInjection.noActiveSession"), "warning");
513972
+ return;
513973
+ }
513974
+ const item = {
513975
+ text: command.input,
513976
+ displayText: command.displayText,
513977
+ origin: channelOrigin(envelope),
513978
+ agentId: this.harness.interactiveAgentId,
513979
+ mode: "channel-command",
513980
+ channelChatId: envelope.meta.chat_id,
513981
+ channelAcknowledge: acknowledge,
513982
+ telegramCommandName: command.name
513983
+ };
513984
+ const busy = this.state.queuedMessages.length > 0 || this.queueSteerInFlight !== void 0 || this.pendingChannelReplyGuard !== void 0 || this.deferUserMessages || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting;
513985
+ if (busy) {
513986
+ this.state.queuedMessages.push(item);
513987
+ this.track("input_queue", { kind: "channel-command" });
513988
+ this.updateQueueDisplay();
513989
+ this.state.ui.requestRender();
513990
+ return;
513991
+ }
513992
+ this.runTelegramRemoteCommand(item);
513993
+ }
513994
+ runTelegramRemoteCommand(item) {
513995
+ this.queueCommandRunning = true;
513996
+ this.preserveQueueAcrossSessionReset = true;
513997
+ if (item.channelTranscriptRendered !== true) this.appendTranscriptEntry({
513998
+ id: nextTranscriptId(),
513999
+ kind: "user",
514000
+ turnId: void 0,
514001
+ renderMode: "plain",
514002
+ content: item.displayText ?? item.text,
514003
+ origin: item.origin
514004
+ });
514005
+ const context = {
514006
+ item,
514007
+ responses: [],
514008
+ sentModelTurn: false
514009
+ };
514010
+ this.telegramRemoteCommandContext = context;
514011
+ const execution = item.telegramCommandName === "befehle" ? Promise.resolve().then(() => {
514012
+ context.responses.push("/loop\n/goal\n/idea\n/befehle");
514013
+ }) : dispatchInput(this, item.text);
514014
+ execution.catch((error) => {
514015
+ this.showError(formatErrorMessage$2(error));
514016
+ }).finally(async () => {
514017
+ if (this.telegramRemoteCommandContext === context) this.telegramRemoteCommandContext = void 0;
514018
+ if (!context.sentModelTurn) {
514019
+ const response = context.responses.at(-1) ?? item.displayText ?? item.text;
514020
+ if (await sendReplyFallback(item.channelChatId, response, false)) item.channelAcknowledge?.();
514021
+ }
514022
+ this.queueCommandRunning = false;
514023
+ if (!this.editorReplacementActive) this.preserveQueueAcrossSessionReset = false;
514024
+ this.updateQueueDisplay();
514025
+ this.state.ui.requestRender();
514026
+ this.scheduleQueueDrain();
514027
+ });
514028
+ }
514029
+ sendTelegramRemoteCommandMessage(session, input, options, context) {
514030
+ context.sentModelTurn = true;
514031
+ this.beginSessionRequest();
514032
+ const previousGuard = this.pendingChannelReplyGuard;
514033
+ const installedGuard = {
514034
+ chatId: context.item.channelChatId,
514035
+ outboxMarker: outboxMarker(),
514036
+ transcriptStart: this.state.transcriptEntries.length,
514037
+ contextOnly: false
514038
+ };
514039
+ this.pendingChannelReplyGuard = installedGuard;
514040
+ this.setAppState({
514041
+ model: BLUN_KING_MODEL_ALIAS,
514042
+ modelFallbackAllowed: false
514043
+ });
514044
+ session.promptAccepted(options?.parts ?? input).then((result) => {
514045
+ if (result.accepted) {
514046
+ context.item.channelAcknowledge?.();
514047
+ return;
514048
+ }
514049
+ if (this.pendingChannelReplyGuard === installedGuard) this.pendingChannelReplyGuard = previousGuard;
514050
+ this.state.queuedMessages = [{
514051
+ ...context.item,
514052
+ channelTranscriptRendered: true
514053
+ }, ...this.state.queuedMessages];
514054
+ this.track("input_queue", { kind: "channel-command" });
514055
+ this.updateQueueDisplay();
514056
+ this.state.ui.requestRender();
514057
+ }).catch((error) => {
514058
+ if (this.pendingChannelReplyGuard === installedGuard) this.pendingChannelReplyGuard = previousGuard;
514059
+ this.failSessionRequest(uiText("blunTui.session.sendFailed", { error: formatErrorMessage$2(error) }));
514060
+ });
514061
+ }
513735
514062
  sendChannelMessageInternal(session, modelInput, displayText, origin, channelChatId, channelImagePath, contextOnly = false, transcriptRendered = false, acknowledge) {
513736
514063
  if (!transcriptRendered) this.appendTranscriptEntry({
513737
514064
  id: nextTranscriptId(),
@@ -513791,6 +514118,7 @@ var BlunTUI = class {
513791
514118
  pendingChannelReplyGuard;
513792
514119
  /** See SessionEventHost.runChannelReplyFallback — called at turn end. */
513793
514120
  channelMediaDeliveries = /* @__PURE__ */ new Set();
514121
+ channelMediaDeliveryFailures = /* @__PURE__ */ new Set();
513794
514122
  /** Deliver completed media at tool-result time so later queued work cannot hide it. */
513795
514123
  runChannelMediaFallback(output) {
513796
514124
  const guard = this.pendingChannelReplyGuard;
@@ -513800,9 +514128,12 @@ var BlunTUI = class {
513800
514128
  if (this.channelMediaDeliveries.has(deliveryKey)) return;
513801
514129
  this.channelMediaDeliveries.add(deliveryKey);
513802
514130
  sendMediaReplyFallback(guard.chatId, filePath).then((sent) => {
513803
- if (sent) return;
514131
+ if (sent) {
514132
+ this.channelMediaDeliveryFailures.delete(deliveryKey);
514133
+ return;
514134
+ }
513804
514135
  this.channelMediaDeliveries.delete(deliveryKey);
513805
- this.showError(uiText("blunTui.session.sendFailed", { error: "Telegram media" }));
514136
+ this.channelMediaDeliveryFailures.add(deliveryKey);
513806
514137
  });
513807
514138
  }
513808
514139
  sendChannelTurnStatus(message) {
@@ -513816,6 +514147,13 @@ var BlunTUI = class {
513816
514147
  const guard = this.pendingChannelReplyGuard;
513817
514148
  if (guard === void 0) return;
513818
514149
  this.pendingChannelReplyGuard = void 0;
514150
+ for (const deliveryKey of [...this.channelMediaDeliveryFailures]) {
514151
+ const separator = deliveryKey.indexOf("\0");
514152
+ if (separator < 0 || deliveryKey.slice(0, separator) !== guard.chatId) continue;
514153
+ const filePath = deliveryKey.slice(separator + 1);
514154
+ this.channelMediaDeliveryFailures.delete(deliveryKey);
514155
+ if (!outboxDeliveredFile(guard.outboxMarker, guard.chatId, filePath)) this.showError(uiText("blunTui.session.sendFailed", { error: "Telegram media" }));
514156
+ }
513819
514157
  if (reason !== "completed") return;
513820
514158
  if (this.state.transcriptEntries.slice(guard.transcriptStart).some((entry) => entry.kind === "tool_call" && /reply|edit_message/i.test(entry.toolCallData?.name ?? ""))) return;
513821
514159
  if (outboxGrewForChat(guard.outboxMarker, guard.chatId)) return;
@@ -513921,7 +514259,7 @@ var BlunTUI = class {
513921
514259
  recallLastQueued() {
513922
514260
  if (this.state.queuedMessages.length === 0) return void 0;
513923
514261
  const last = this.state.queuedMessages.at(-1);
513924
- if (last.mode === "channel") return void 0;
514262
+ if (last.mode === "channel" || last.mode === "channel-command") return void 0;
513925
514263
  const queuedBatchCount = Math.max(0, this.queueFlushBatchRemaining - (this.queueSteerInFlight?.items.length ?? 0));
513926
514264
  const removesBatchItem = this.state.queuedMessages.length <= queuedBatchCount;
513927
514265
  this.state.queuedMessages = this.state.queuedMessages.slice(0, -1);
@@ -513979,6 +514317,10 @@ var BlunTUI = class {
513979
514317
  this.runSlashCommand(item.text);
513980
514318
  return;
513981
514319
  }
514320
+ if (item.mode === "channel-command") {
514321
+ this.runTelegramRemoteCommand(item);
514322
+ return;
514323
+ }
513982
514324
  const activeSession = this.session ?? session;
513983
514325
  if (item.mode === "channel") {
513984
514326
  this.harness.withInteractiveAgent(item.agentId ?? "main", () => {
@@ -514051,6 +514393,11 @@ var BlunTUI = class {
514051
514393
  });
514052
514394
  }
514053
514395
  sendMessage(session, input, options) {
514396
+ const telegramContext = this.telegramRemoteCommandContext;
514397
+ if (telegramContext !== void 0) {
514398
+ this.sendTelegramRemoteCommandMessage(session, input, options, telegramContext);
514399
+ return;
514400
+ }
514054
514401
  if (this.deferUserMessages || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting) {
514055
514402
  this.enqueueMessage(input, options);
514056
514403
  return;
@@ -514214,7 +514561,7 @@ var BlunTUI = class {
514214
514561
  if (this.state.appState.additionalDirs.length > 0) options.additionalDirs = [...this.state.appState.additionalDirs];
514215
514562
  return this.harness.createSession(options);
514216
514563
  }
514217
- async setSession(session) {
514564
+ async setSession(session, options = {}) {
514218
514565
  await session.setModel(BLUN_KING_MODEL_ALIAS, { allowFallback: false });
514219
514566
  await this.personalMemoryController.clear(this.session);
514220
514567
  const previous = this.unloadCurrentSession(approvalCancellationFeedback("switching_session"));
@@ -514232,10 +514579,10 @@ var BlunTUI = class {
514232
514579
  this.syncAdditionalDirs(session);
514233
514580
  await this.refreshManagedImageReaderAvailability(session);
514234
514581
  await this.authFlow.applyManagedAccountContextToSession();
514235
- await this.personalMemoryController.refresh();
514582
+ if (options.refreshPersonalMemory !== false) await this.personalMemoryController.refresh();
514236
514583
  }
514237
514584
  async syncRuntimeState(session = this.requireSession()) {
514238
- const [status, goalResult] = await Promise.all([session.getStatus(), session.getGoal()]);
514585
+ const [status, goalResult, loopResult] = await Promise.all([session.getStatus(), session.getGoal(), session.getLoop()]);
514239
514586
  this.setAppState({
514240
514587
  sessionId: session.id,
514241
514588
  model: BLUN_KING_MODEL_ALIAS,
@@ -514252,11 +514599,22 @@ var BlunTUI = class {
514252
514599
  compactionBudgetTokens: status.compactionBudgetTokens ?? 0,
514253
514600
  contextUsage: status.contextUsage,
514254
514601
  sessionTitle: session.summary?.title ?? null,
514255
- goal: goalResult.goal
514602
+ goal: goalResult.goal,
514603
+ loop: loopResult.loop
514256
514604
  });
514257
514605
  this.syncAdditionalDirs(session);
514258
514606
  this.startTelegramChannel();
514259
514607
  }
514608
+ async refreshLoopState(session = this.session) {
514609
+ if (session === void 0) {
514610
+ this.setAppState({ loop: null });
514611
+ return;
514612
+ }
514613
+ try {
514614
+ const { loop } = await session.getLoop();
514615
+ if (this.session === session) this.setAppState({ loop });
514616
+ } catch {}
514617
+ }
514260
514618
  async applyStartupModesToResumedSession(session) {
514261
514619
  const { startup } = this.options;
514262
514620
  if (startup.auto) await session.setPermission("auto");
@@ -514297,6 +514655,7 @@ var BlunTUI = class {
514297
514655
  this.harness.setTelemetryContext({ sessionId: null });
514298
514656
  this.setAppState({
514299
514657
  goal: null,
514658
+ loop: null,
514300
514659
  swarmModeEntry: void 0
514301
514660
  });
514302
514661
  return previous;
@@ -514343,7 +514702,7 @@ var BlunTUI = class {
514343
514702
  streamingPhase: "idle"
514344
514703
  });
514345
514704
  if (!this.preserveQueueAcrossSessionReset) {
514346
- this.state.queuedMessages = this.state.queuedMessages.filter((item) => item.mode === "channel");
514705
+ this.state.queuedMessages = this.state.queuedMessages.filter((item) => item.mode === "channel" || item.mode === "channel-command");
514347
514706
  this.queueFlushBatchRemaining = 0;
514348
514707
  this.queueSteerInFlight = void 0;
514349
514708
  }
@@ -514772,10 +515131,12 @@ var BlunTUI = class {
514772
515131
  children.splice(0, children.length, ...newChildren);
514773
515132
  }
514774
515133
  showStatus(message, color) {
515134
+ this.telegramRemoteCommandContext?.responses.push(message);
514775
515135
  this.state.transcriptContainer.addChild(new StatusMessageComponent(message, color));
514776
515136
  this.state.ui.requestRender();
514777
515137
  }
514778
515138
  showNotice(title, detail) {
515139
+ this.telegramRemoteCommandContext?.responses.push(detail === void 0 ? title : `${title}\n${detail}`);
514779
515140
  this.state.transcriptContainer.addChild(new NoticeMessageComponent(title, detail));
514780
515141
  this.state.ui.requestRender();
514781
515142
  }
@@ -515764,7 +516125,8 @@ async function runShell(opts, version, updateStartupNotice) {
515764
516125
  duration_ms: Date.now() - startedAt,
515765
516126
  config_ms: configMs,
515766
516127
  init_ms: initMs,
515767
- mcp_ms: mcpMs
516128
+ mcp_ms: mcpMs,
516129
+ ...tui.getStartupPhaseMs()
515768
516130
  });
515769
516131
  } catch (error) {
515770
516132
  removeCrashHandlers();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.50",
3
+ "version": "9.1.52",
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": {
@@ -73603,8 +73603,9 @@ async function runTool(api, token, name, args) {
73603
73603
  * Idempotency guard: models occasionally call reply multiple times with the
73604
73604
  * same text. A message that
73605
73605
  * innerhalb des Fensters bereits wortgleich an denselben Chat ging, wird nicht
73606
- * erneut gesendet — das Tool meldet dem Modell "bereits zugestellt", damit es
73607
- * nicht weiter retryt. Datei-Anhänge sind nie betroffen.
73606
+ * erneut gesendet — das Tool meldet dem Modell "bereits zugestellt", damit es
73607
+ * nicht weiter retryt. Bereits automatisch gelieferte Dateien werden aus
73608
+ * einem direkt folgenden manuellen Reply entfernt.
73608
73609
  */
73609
73610
  const DUPLICATE_WINDOW_MS = 3e5;
73610
73611
  const OUTBOX_TAIL_BYTES = 32768;
@@ -73629,13 +73630,49 @@ function isRecentDuplicate(chatId, text) {
73629
73630
  } catch {}
73630
73631
  return false;
73631
73632
  }
73633
+ function recentlyDeliveredFiles(chatId, files) {
73634
+ if (files.length === 0) return /* @__PURE__ */ new Set();
73635
+ const wanted = new Map(files.map((file) => [resolve(file).toLowerCase(), file]));
73636
+ const delivered = /* @__PURE__ */ new Set();
73637
+ try {
73638
+ const file = outboxLog();
73639
+ const size = statSync(file).size;
73640
+ const raw = readFileSync(file, "utf8");
73641
+ const tail = size > OUTBOX_TAIL_BYTES ? raw.slice(raw.length - OUTBOX_TAIL_BYTES) : raw;
73642
+ const cutoff = Date.now() - DUPLICATE_WINDOW_MS;
73643
+ for (const line of tail.split("\n")) {
73644
+ if (line.trim().length === 0) continue;
73645
+ try {
73646
+ const entry = JSON.parse(line);
73647
+ if (String(entry.chat_id) !== String(chatId)) continue;
73648
+ if (entry.ts === void 0 || Date.parse(entry.ts) < cutoff || !Array.isArray(entry.files)) continue;
73649
+ for (const priorFile of entry.files) {
73650
+ if (typeof priorFile !== "string") continue;
73651
+ const original = wanted.get(resolve(priorFile).toLowerCase());
73652
+ if (original !== void 0) delivered.add(original);
73653
+ }
73654
+ } catch {}
73655
+ }
73656
+ } catch {}
73657
+ return delivered;
73658
+ }
73632
73659
  async function runReply(api, args) {
73633
73660
  const chatId = args["chat_id"];
73634
73661
  const text = args["text"];
73635
73662
  const replyTo = args["reply_to"] !== null && args["reply_to"] !== void 0 ? Number(args["reply_to"]) : void 0;
73636
- const files = args["files"] ?? [];
73663
+ let files = args["files"] ?? [];
73637
73664
  const parseMode = args["format"] === "markdownv2" ? "MarkdownV2" : void 0;
73638
73665
  assertAllowedChat(chatId);
73666
+ const duplicateFiles = recentlyDeliveredFiles(chatId, files);
73667
+ if (duplicateFiles.size > 0) {
73668
+ files = files.filter((file) => !duplicateFiles.has(file));
73669
+ appendJsonl(outboxLog(), {
73670
+ direction: "out",
73671
+ kind: "reply-file-duplicate-suppressed",
73672
+ chat_id: chatId,
73673
+ files: [...duplicateFiles]
73674
+ });
73675
+ }
73639
73676
  if (files.length === 0 && isGroupChat(chatId) && isGroupNoiseReply(text)) {
73640
73677
  appendJsonl(outboxLog(), {
73641
73678
  direction: "out",