@link-assistant/hive-mind 1.78.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 70e1542: fix(retry): treat 5-hour "session limit" and "weekly limit" 429s as account usage limits, not transient throttles (#1935)
8
+
9
+ A long-running solve session (588 turns, ~$70.62) hit Claude's **5-hour session
10
+ limit**. The Claude CLI surfaced it as a `result` event with `is_error: true`,
11
+ `api_error_status: 429`, and:
12
+
13
+ ```
14
+ You've hit your session limit · resets 4pm (UTC)
15
+ ```
16
+
17
+ Instead of being treated as an **account usage limit** (post a comment with the
18
+ reset time + wait until the exact reset moment), it was put through the transient
19
+ exponential-backoff retry loop:
20
+
21
+ ```
22
+ ⚠️ Server rate limited (429) detected. Retry 1/10 in 2 min (session preserved)...
23
+ Error: You've hit your session limit · resets 4pm (UTC)
24
+ ⚠️ Server rate limited (429) detected. Retry 2/10 in 4 min (session preserved)...
25
+ ```
26
+
27
+ Each retry re-hit the same limit because the quota only frees at the reset time —
28
+ so the harness burned ~10 futile retries and never told the user when the limit
29
+ resets.
30
+
31
+ Root cause (regression from #1924): `src/claude.lib.mjs` set
32
+ `isRateLimitError = true` for **every** structured `api_error_status === 429`,
33
+ without checking whether the message was an account usage limit. Claude reports
34
+ **both** a transient throttle ("...not your usage limit...") and account
35
+ session/weekly limits with `api_error_status: 429`, so the unconditional check
36
+ swept genuine usage limits into the transient-retry path — ahead of the
37
+ `detectUsageLimit()` reset-time wait, which was therefore never reached.
38
+
39
+ Fix: `src/claude.lib.mjs` now only flags a structured 429 as a transient rate
40
+ limit when the message is **not** a usage limit
41
+ (`api_error_status === 429 && !isUsageLimitError(lastMessage)`), so session/weekly
42
+ limits fall through to the usage-limit handler that immediately posts a comment
43
+ and waits until the exact reset time (auto-resuming there with
44
+ `--auto-continue-limit`). `src/usage-limit.lib.mjs` additionally recognises the
45
+ "hit your session limit" / "hit your weekly limit" phrasing as a backstop (the
46
+ reset-time regex already matched "resets 4pm").
47
+
48
+ Added `tests/test-issue-1935-session-limit-429.mjs` (15 assertions) and a full
49
+ case study with timeline, blame history (PR #1924), root-cause analysis, and the
50
+ captured logs under `docs/case-studies/issue-1935`.
51
+
52
+ ## 2.0.0
53
+
54
+ ### Major Changes
55
+
56
+ - fd84e85: Rename the cleanup executable to `hive-cleanup` and harden destructive confirmation parsing against hidden terminal control input.
57
+
3
58
  ## 1.78.13
4
59
 
5
60
  ### Patch Changes
package/README.hi.md CHANGED
@@ -735,7 +735,7 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
735
735
 
736
736
  ### डिस्क क्लीनअप
737
737
 
738
- `cleanup` पुरानी hive-mind अस्थायी डायरेक्टरी/फ़ाइलों (जैसे प्रति-कार्य क्लोन
738
+ `hive-cleanup` पुरानी hive-mind अस्थायी डायरेक्टरी/फ़ाइलों (जैसे प्रति-कार्य क्लोन
739
739
  `/tmp/gh-issue-solver-*`, MCP कॉन्फ़िग फ़ाइलें, लॉग डाउनलोड डायरेक्टरी आदि) को हटाकर
740
740
  डिस्क स्थान खाली करता है, जबकि **वर्तमान में चल रहे कार्यों से संबंधित फ़ोल्डर**,
741
741
  सुरक्षित सिस्टम पथ, और बिना कमिट या बिना पुश किए बदलावों वाले किसी भी क्लोन को बनाए
@@ -745,40 +745,40 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
745
745
 
746
746
  ```bash
747
747
  # पूर्वावलोकन: रखे जाने वाले और हटाए जाने वाले फ़ोल्डरों की सूची (कुछ भी नहीं हटाता)
748
- cleanup --dry-run
748
+ hive-cleanup --dry-run
749
749
 
750
750
  # पुरानी अस्थायी फ़ाइलें वास्तव में हटाएँ (पहले पुष्टि माँगता है)
751
- cleanup
751
+ hive-cleanup
752
752
 
753
753
  # पुष्टि प्रॉम्प्ट के बिना हटाएँ
754
- cleanup --force
754
+ hive-cleanup --force
755
755
 
756
756
  # गैर-hive-mind अस्थायी प्रविष्टियों पर भी विचार करें (अधिक आक्रामक)
757
- cleanup --all --dry-run
757
+ hive-cleanup --all --dry-run
758
758
 
759
759
  # /tmp/start-command को हटाने की अनुमति दें (डिफ़ॉल्ट रूप से रखा जाता है; इसमें आइसोलेशन लॉग होते हैं)
760
- cleanup --force-start-command
760
+ hive-cleanup --force-start-command
761
761
 
762
762
  # Ubuntu / सिस्टम क्लीनअप (apt कैश, journald लॉग, npm कैश)
763
- cleanup --system --sudo
763
+ hive-cleanup --system --sudo
764
764
 
765
765
  # लाइव/अटके हुए agent PID को hive/start-command कार्य सत्रों से मिलाएँ
766
- cleanup --processes
766
+ hive-cleanup --processes
767
767
 
768
768
  # किसी खास non-agent PID को ट्रेस करें, जैसे browser child या shell
769
- cleanup --pid 94445
769
+ hive-cleanup --pid 94445
770
770
 
771
771
  # रोके जा सकने वाले orphaned agents का पूर्वावलोकन करें
772
- cleanup --kill-orphaned-agents --dry-run
772
+ hive-cleanup --kill-orphaned-agents --dry-run
773
773
 
774
774
  # पूर्वावलोकन जाँचने के बाद orphaned agent process trees रोकें
775
- cleanup --kill-orphaned-agents --force
775
+ hive-cleanup --kill-orphaned-agents --force
776
776
 
777
777
  # सक्रिय-कार्य पहचान अक्षम करें (केवल सुरक्षित पथ रखे जाते हैं)
778
- cleanup --no-keep-active-tasks-folders --dry-run
778
+ hive-cleanup --no-keep-active-tasks-folders --dry-run
779
779
  ```
780
780
 
781
- विकल्पों की पूरी सूची के लिए `cleanup --help` चलाएँ। यह कमांड dry-run के अनुकूल है और
781
+ विकल्पों की पूरी सूची के लिए `hive-cleanup --help` चलाएँ। यह कमांड dry-run के अनुकूल है और
782
782
  हर रन के लिए टाइमस्टैम्प वाला `cleanup-*.log` लिखता है। प्रक्रिया डायग्नोस्टिक आउटपुट
783
783
  कमांड लाइन प्रिंट करने से पहले सामान्य token आकारों को छिपाता है।
784
784
 
@@ -836,14 +836,14 @@ find docs/ -name "*.md" -exec wc -l {} + | awk '$1 > 1000 {print "ERROR: " $2 "
836
836
 
837
837
  ```bash
838
838
  # agent PID, start-command session ID, GitHub task URL, workspace, match reasons और संभावित orphaned agents दिखाएँ।
839
- cleanup --processes
839
+ hive-cleanup --processes
840
840
 
841
841
  # उसी report में कोई भी PID शामिल करें।
842
- cleanup --pid 62220
842
+ hive-cleanup --pid 62220
843
843
 
844
844
  # केवल terminal task वाले orphaned agents रोकें।
845
- cleanup --kill-orphaned-agents --dry-run
846
- cleanup --kill-orphaned-agents --force
845
+ hive-cleanup --kill-orphaned-agents --dry-run
846
+ hive-cleanup --kill-orphaned-agents --force
847
847
  ```
848
848
 
849
849
  Manual fallback: उन स्क्रीन की पहचान करें जो संसाधन खपत करने वाली प्रक्रियाओं के पैरेंट हैं।
package/README.md CHANGED
@@ -755,7 +755,7 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
755
755
 
756
756
  ### Disk Cleanup
757
757
 
758
- `cleanup` frees disk space by removing stale hive-mind temporary
758
+ `hive-cleanup` frees disk space by removing stale hive-mind temporary
759
759
  directories/files (per-task clones like `/tmp/gh-issue-solver-*`, MCP config
760
760
  files, log download dirs, …) while **keeping folders that belong to
761
761
  currently-running tasks**, protected system paths, and any clone with
@@ -766,40 +766,40 @@ branch).
766
766
 
767
767
  ```bash
768
768
  # Preview: list kept folders and folders that would be deleted (deletes nothing)
769
- cleanup --dry-run
769
+ hive-cleanup --dry-run
770
770
 
771
771
  # Actually delete stale temp artifacts (asks for confirmation first)
772
- cleanup
772
+ hive-cleanup
773
773
 
774
774
  # Delete without the confirmation prompt
775
- cleanup --force
775
+ hive-cleanup --force
776
776
 
777
777
  # Also consider non-hive-mind temp entries (more aggressive)
778
- cleanup --all --dry-run
778
+ hive-cleanup --all --dry-run
779
779
 
780
780
  # Allow deleting /tmp/start-command (kept by default; holds isolation logs)
781
- cleanup --force-start-command
781
+ hive-cleanup --force-start-command
782
782
 
783
783
  # Ubuntu / system cleanup (apt caches, journald logs, npm cache)
784
- cleanup --system --sudo
784
+ hive-cleanup --system --sudo
785
785
 
786
786
  # Map live/stuck agent PIDs back to hive/start-command task sessions
787
- cleanup --processes
787
+ hive-cleanup --processes
788
788
 
789
789
  # Trace a specific non-agent PID, for example a browser child or shell
790
- cleanup --pid 94445
790
+ hive-cleanup --pid 94445
791
791
 
792
792
  # Preview orphaned terminal-session agents that can be stopped
793
- cleanup --kill-orphaned-agents --dry-run
793
+ hive-cleanup --kill-orphaned-agents --dry-run
794
794
 
795
795
  # Stop orphaned agent process trees after reviewing the preview
796
- cleanup --kill-orphaned-agents --force
796
+ hive-cleanup --kill-orphaned-agents --force
797
797
 
798
798
  # Disable active-task detection (only protected paths are kept)
799
- cleanup --no-keep-active-tasks-folders --dry-run
799
+ hive-cleanup --no-keep-active-tasks-folders --dry-run
800
800
  ```
801
801
 
802
- Run `cleanup --help` for the full list of options. The command is dry-run
802
+ Run `hive-cleanup --help` for the full list of options. The command is dry-run
803
803
  friendly and writes a timestamped `cleanup-*.log` for every run. Process
804
804
  diagnostic output redacts common token shapes before printing command lines.
805
805
 
@@ -859,14 +859,14 @@ that launched it:
859
859
  ```bash
860
860
  # Show agent PIDs, start-command session IDs, GitHub task URLs, workspaces,
861
861
  # match reasons, and possible orphaned terminal-session agents.
862
- cleanup --processes
862
+ hive-cleanup --processes
863
863
 
864
864
  # Include an arbitrary PID in the same report.
865
- cleanup --pid 62220
865
+ hive-cleanup --pid 62220
866
866
 
867
867
  # Kill only agents whose matched start-command task is already terminal.
868
- cleanup --kill-orphaned-agents --dry-run
869
- cleanup --kill-orphaned-agents --force
868
+ hive-cleanup --kill-orphaned-agents --dry-run
869
+ hive-cleanup --kill-orphaned-agents --force
870
870
  ```
871
871
 
872
872
  Manual fallback: identify screens that are parents of processes that are eating
package/README.ru.md CHANGED
@@ -737,7 +737,7 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
737
737
 
738
738
  ### Очистка диска
739
739
 
740
- `cleanup` освобождает место на диске, удаляя устаревшие временные каталоги/файлы
740
+ `hive-cleanup` освобождает место на диске, удаляя устаревшие временные каталоги/файлы
741
741
  hive-mind (клоны для каждой задачи вида `/tmp/gh-issue-solver-*`, файлы
742
742
  конфигурации MCP, каталоги загрузки логов и т. д.), при этом **сохраняя папки,
743
743
  относящиеся к выполняющимся в данный момент задачам**, защищённые системные пути и
@@ -748,40 +748,40 @@ hive-mind (клоны для каждой задачи вида `/tmp/gh-issue-s
748
748
 
749
749
  ```bash
750
750
  # Предпросмотр: список сохраняемых и удаляемых папок (ничего не удаляет)
751
- cleanup --dry-run
751
+ hive-cleanup --dry-run
752
752
 
753
753
  # Реально удалить устаревшие временные файлы (сначала запросит подтверждение)
754
- cleanup
754
+ hive-cleanup
755
755
 
756
756
  # Удалить без запроса подтверждения
757
- cleanup --force
757
+ hive-cleanup --force
758
758
 
759
759
  # Учитывать также не-hive-mind временные записи (более агрессивно)
760
- cleanup --all --dry-run
760
+ hive-cleanup --all --dry-run
761
761
 
762
762
  # Разрешить удаление /tmp/start-command (по умолчанию сохраняется; хранит логи изоляции)
763
- cleanup --force-start-command
763
+ hive-cleanup --force-start-command
764
764
 
765
765
  # Очистка Ubuntu / системы (кэши apt, логи journald, кэш npm)
766
- cleanup --system --sudo
766
+ hive-cleanup --system --sudo
767
767
 
768
768
  # Сопоставить живые/зависшие agent PID с задачами hive/start-command
769
- cleanup --processes
769
+ hive-cleanup --processes
770
770
 
771
771
  # Отследить конкретный не-agent PID, например дочерний процесс браузера или shell
772
- cleanup --pid 94445
772
+ hive-cleanup --pid 94445
773
773
 
774
774
  # Предпросмотр orphaned agents, которые можно остановить
775
- cleanup --kill-orphaned-agents --dry-run
775
+ hive-cleanup --kill-orphaned-agents --dry-run
776
776
 
777
777
  # Остановить деревья orphaned agent процессов после просмотра предпросмотра
778
- cleanup --kill-orphaned-agents --force
778
+ hive-cleanup --kill-orphaned-agents --force
779
779
 
780
780
  # Отключить обнаружение активных задач (сохраняются только защищённые пути)
781
- cleanup --no-keep-active-tasks-folders --dry-run
781
+ hive-cleanup --no-keep-active-tasks-folders --dry-run
782
782
  ```
783
783
 
784
- Запустите `cleanup --help`, чтобы увидеть полный список опций. Команда удобна для
784
+ Запустите `hive-cleanup --help`, чтобы увидеть полный список опций. Команда удобна для
785
785
  режима dry-run и записывает лог `cleanup-*.log` с меткой времени при каждом запуске.
786
786
  Диагностический вывод процессов скрывает распространённые формы token перед печатью
787
787
  командных строк.
@@ -841,14 +841,14 @@ find docs/ -name "*.md" -exec wc -l {} + | awk '$1 > 1000 {print "ERROR: " $2 "
841
841
  ```bash
842
842
  # Показать agent PID, start-command session ID, GitHub task URL, workspace,
843
843
  # причины совпадения и возможные orphaned agents.
844
- cleanup --processes
844
+ hive-cleanup --processes
845
845
 
846
846
  # Включить произвольный PID в тот же отчёт.
847
- cleanup --pid 62220
847
+ hive-cleanup --pid 62220
848
848
 
849
849
  # Остановить только orphaned agents, чья задача уже завершена.
850
- cleanup --kill-orphaned-agents --dry-run
851
- cleanup --kill-orphaned-agents --force
850
+ hive-cleanup --kill-orphaned-agents --dry-run
851
+ hive-cleanup --kill-orphaned-agents --force
852
852
  ```
853
853
 
854
854
  Ручной fallback: определите screen-сессии, являющиеся родительскими для процессов,
package/README.zh.md CHANGED
@@ -730,7 +730,7 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
730
730
 
731
731
  ### 磁盘清理
732
732
 
733
- `cleanup` 通过删除过时的 hive-mind 临时目录/文件(如每个任务的克隆
733
+ `hive-cleanup` 通过删除过时的 hive-mind 临时目录/文件(如每个任务的克隆
734
734
  `/tmp/gh-issue-solver-*`、MCP 配置文件、日志下载目录等)来释放磁盘空间,同时
735
735
  **保留属于当前正在运行任务的文件夹**、受保护的系统路径,以及任何包含未提交或未推送
736
736
  更改的克隆。它通过运行中的进程和实时隔离会话检测活动任务,并使用与 `solve` 相同的
@@ -739,40 +739,40 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
739
739
 
740
740
  ```bash
741
741
  # 预览:列出保留的文件夹和将被删除的文件夹(不删除任何内容)
742
- cleanup --dry-run
742
+ hive-cleanup --dry-run
743
743
 
744
744
  # 实际删除过时的临时文件(会先要求确认)
745
- cleanup
745
+ hive-cleanup
746
746
 
747
747
  # 删除时不显示确认提示
748
- cleanup --force
748
+ hive-cleanup --force
749
749
 
750
750
  # 同时考虑非 hive-mind 临时项(更激进)
751
- cleanup --all --dry-run
751
+ hive-cleanup --all --dry-run
752
752
 
753
753
  # 允许删除 /tmp/start-command(默认保留;其中存放隔离日志)
754
- cleanup --force-start-command
754
+ hive-cleanup --force-start-command
755
755
 
756
756
  # Ubuntu / 系统清理(apt 缓存、journald 日志、npm 缓存)
757
- cleanup --system --sudo
757
+ hive-cleanup --system --sudo
758
758
 
759
759
  # 将实时/卡住的 agent PID 映射回 hive/start-command 任务会话
760
- cleanup --processes
760
+ hive-cleanup --processes
761
761
 
762
762
  # 跟踪指定的非 agent PID,例如浏览器子进程或 shell
763
- cleanup --pid 94445
763
+ hive-cleanup --pid 94445
764
764
 
765
765
  # 预览可以停止的孤立 agent
766
- cleanup --kill-orphaned-agents --dry-run
766
+ hive-cleanup --kill-orphaned-agents --dry-run
767
767
 
768
768
  # 审查预览后停止孤立 agent 进程树
769
- cleanup --kill-orphaned-agents --force
769
+ hive-cleanup --kill-orphaned-agents --force
770
770
 
771
771
  # 禁用活动任务检测(仅保留受保护的路径)
772
- cleanup --no-keep-active-tasks-folders --dry-run
772
+ hive-cleanup --no-keep-active-tasks-folders --dry-run
773
773
  ```
774
774
 
775
- 运行 `cleanup --help` 查看完整的选项列表。该命令对 dry-run 友好,并为每次运行写入
775
+ 运行 `hive-cleanup --help` 查看完整的选项列表。该命令对 dry-run 友好,并为每次运行写入
776
776
  带时间戳的 `cleanup-*.log` 日志。进程诊断输出会在打印命令行前遮蔽常见 token
777
777
  格式。
778
778
 
@@ -830,14 +830,14 @@ hive 任务时,优先使用内置进程诊断命令:
830
830
 
831
831
  ```bash
832
832
  # 显示 agent PID、start-command 会话 ID、GitHub 任务 URL、工作区、匹配原因和可能的孤立 agent。
833
- cleanup --processes
833
+ hive-cleanup --processes
834
834
 
835
835
  # 在同一报告中包含任意 PID。
836
- cleanup --pid 62220
836
+ hive-cleanup --pid 62220
837
837
 
838
838
  # 只停止已完成任务中的孤立 agent。
839
- cleanup --kill-orphaned-agents --dry-run
840
- cleanup --kill-orphaned-agents --force
839
+ hive-cleanup --kill-orphaned-agents --dry-run
840
+ hive-cleanup --kill-orphaned-agents --force
841
841
  ```
842
842
 
843
843
  手动兜底:识别消耗资源的进程所属的 Screen 会话。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.78.13",
3
+ "version": "2.0.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "hive": "./src/hive.mjs",
9
9
  "solve": "./src/solve.mjs",
10
10
  "task": "./src/task.mjs",
11
- "cleanup": "./src/cleanup.mjs",
11
+ "hive-cleanup": "./src/cleanup.mjs",
12
12
  "review": "./src/review.mjs",
13
13
  "configure-claude": "./src/configure-claude.mjs",
14
14
  "start-screen": "./src/start-screen.mjs",
@@ -9,7 +9,7 @@ const path = (await use('path')).default;
9
9
  import { log, isENOSPC } from './lib.mjs';
10
10
  import { reportError } from './sentry.lib.mjs';
11
11
  import { timeouts, retryLimits, claudeCode, getClaudeEnv, getThinkingLevelToTokens, getTokensToThinkingLevel, supportsThinkingBudget, DEFAULT_MAX_THINKING_BUDGET, getMaxOutputTokensForModel } from './config.lib.mjs';
12
- import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
12
+ import { detectUsageLimit, formatUsageLimitMessage, isUsageLimitError } from './usage-limit.lib.mjs';
13
13
  import { createInteractiveHandler } from './interactive-mode.lib.mjs';
14
14
  import { setupBidirectionalHandler, finalizeBidirectionalHandler, validateBidirectionalModeConfig, attachStreamingInput } from './bidirectional-interactive.lib.mjs';
15
15
  import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
@@ -978,11 +978,12 @@ export const executeClaudeCommand = async params => {
978
978
  isRequestTimeout = true;
979
979
  await log('⏱️ Detected request timeout from Claude CLI (will retry with --resume)', { verbose: true });
980
980
  }
981
- // Issue #1924: Server-side temporary rate limiting (HTTP 429) a transient
982
- // throttle, not an account usage limit ("...not your usage limit..."), so retry
983
- // with --resume. The message text is handled by classifyRetryableError; this also
984
- // catches the structured api_error_status if the wording ever changes.
985
- if (data.api_error_status === 429) {
981
+ // Issue #1924: server-side temporary rate limiting (HTTP 429) is a transient
982
+ // throttle ("...not your usage limit..."), so retry with --resume. Issue #1935
983
+ // (regression from #1924): account usage limits ("session limit" / "weekly limit")
984
+ // ALSO arrive with api_error_status === 429 plus an explicit reset time, so the
985
+ // isUsageLimitError() guard routes those to the usage-limit handler below instead.
986
+ if (data.api_error_status === 429 && !isUsageLimitError(lastMessage)) {
986
987
  isRateLimitError = true;
987
988
  await log(`⚠️ Detected server-side rate limiting (429) from Claude CLI (will retry with --resume). request_id=${data.request_id || 'unknown'}`, { verbose: true });
988
989
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Core, offline-testable logic for the `cleanup` command (issue #1848).
2
+ * Core, offline-testable logic for the `hive-cleanup` command (issue #1848).
3
3
  *
4
4
  * This module deliberately avoids any top-level network access (no `use-m`
5
5
  * fetch) and any side effects so it can be unit-tested without a network
@@ -144,8 +144,8 @@ function sameRepo(a, b) {
144
144
  * fall back to the `issue-{number}-{hex}` prefix (the random hex is unknown from
145
145
  * the URL alone) combined with a repo match.
146
146
  *
147
- * @param {Array<{owner, repo, type, number, branch?: string|null}>} activeTasks
148
- * @returns {Array<{owner, repo, type, issueNumber: number|null, branch: string|null}>}
147
+ * @param {Array<{owner, repo, type, number, branch?: string|null, sessionId?: string|null, sessionName?: string|null, status?: string|null, workspace?: string|null}>} activeTasks
148
+ * @returns {Array<{owner, repo, type, number: number|null, issueNumber: number|null, branch: string|null, sessionId: string|null, sessionName: string|null, status: string|null, workspace: string|null}>}
149
149
  */
150
150
  export function buildActiveMatchers(activeTasks) {
151
151
  const matchers = [];
@@ -155,8 +155,13 @@ export function buildActiveMatchers(activeTasks) {
155
155
  owner: task.owner,
156
156
  repo: task.repo,
157
157
  type: task.type,
158
+ number: task.number ?? null,
158
159
  issueNumber: task.type === 'issue' ? task.number : (task.issueNumber ?? null),
159
160
  branch: task.branch || null,
161
+ sessionId: task.sessionId || null,
162
+ sessionName: task.sessionName || null,
163
+ status: task.status || null,
164
+ workspace: task.workspace || null,
160
165
  });
161
166
  }
162
167
  return matchers;
@@ -291,9 +296,12 @@ export function classifyEntry(entry, ctx) {
291
296
  export function classifyEntries(entries, ctx) {
292
297
  const keep = [];
293
298
  const remove = [];
299
+ const { matchers = [], gitInfoByPath = new Map() } = ctx || {};
294
300
  for (const entry of entries || []) {
295
301
  const { action, reason } = classifyEntry(entry, ctx);
296
- const record = { name: entry.name, path: entry.path, size: entry.size ?? null, reason };
302
+ const gitInfo = gitInfoByPath.get(entry.path) || null;
303
+ const activeTask = reason === 'active-task' ? folderMatchesActiveTask(gitInfo, matchers) : null;
304
+ const record = { name: entry.name, path: entry.path, size: entry.size ?? null, reason, gitInfo, activeTask };
297
305
  if (action === 'remove') remove.push(record);
298
306
  else keep.push(record);
299
307
  }
@@ -357,3 +365,51 @@ export function describeReason(reason) {
357
365
  };
358
366
  return map[reason] || reason;
359
367
  }
368
+
369
+ function firstRemote(gitInfo) {
370
+ return gitInfo?.remotes?.[0] || null;
371
+ }
372
+
373
+ function compactTaskType(type) {
374
+ return type === 'pull' ? 'PR' : 'issue';
375
+ }
376
+
377
+ /**
378
+ * Format an active task for logs.
379
+ *
380
+ * @param {{owner?: string, repo?: string, type?: string, number?: number|null, issueNumber?: number|null, branch?: string|null, sessionId?: string|null, sessionName?: string|null, status?: string|null, workspace?: string|null}} task
381
+ * @returns {string}
382
+ */
383
+ export function formatTaskSummary(task) {
384
+ if (!task) return '';
385
+ const number = task.number ?? task.issueNumber ?? null;
386
+ const parts = [`${task.owner}/${task.repo} ${compactTaskType(task.type)} #${number ?? '?'}`];
387
+ if (task.branch) parts.push(`branch ${task.branch}`);
388
+ if (task.sessionId || task.sessionName) parts.push(`session ${task.sessionId || task.sessionName}`);
389
+ if (task.status) parts.push(`status ${task.status}`);
390
+ if (task.workspace) parts.push(`workspace ${task.workspace}`);
391
+ return parts.join(', ');
392
+ }
393
+
394
+ /**
395
+ * Format per-entry git/task context for one-line cleanup reports.
396
+ *
397
+ * @param {{gitInfo?: {branch?: string|null, remotes?: Array<{owner, repo}>|null, dirty?: boolean}|null, activeTask?: Object|null}} item
398
+ * @returns {string}
399
+ */
400
+ export function formatEntryContext(item) {
401
+ const details = [];
402
+ if (item?.activeTask) details.push(`task ${formatTaskSummary(item.activeTask)}`);
403
+
404
+ const gitInfo = item?.gitInfo;
405
+ if (gitInfo) {
406
+ const remote = firstRemote(gitInfo);
407
+ const gitParts = [];
408
+ if (remote) gitParts.push(`repo ${remote.owner}/${remote.repo}`);
409
+ if (gitInfo.branch) gitParts.push(`branch ${gitInfo.branch}`);
410
+ if (gitInfo.dirty) gitParts.push('dirty/unpushed');
411
+ if (gitParts.length > 0) details.push(gitParts.join(', '));
412
+ }
413
+
414
+ return details.length > 0 ? ` (${details.join('; ')})` : '';
415
+ }
package/src/cleanup.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `cleanup` — free disk space by removing stale hive-mind temporary
3
+ * `hive-cleanup` — free disk space by removing stale hive-mind temporary
4
4
  * directories/files while preserving folders that belong to currently-running
5
5
  * (active) tasks, protected system paths and any work that is not yet pushed.
6
6
  *
@@ -33,9 +33,9 @@
33
33
 
34
34
  import path from 'node:path';
35
35
  import { promises as fsp } from 'node:fs';
36
- import { execSync } from 'node:child_process';
37
36
 
38
- import { classifyEntries, summarize, formatBytes, describeReason, buildActiveMatchers, DEFAULT_PROTECTED_NAMES } from './cleanup.lib.mjs';
37
+ import { isConfirmationYes, readConfirmationLine } from './confirmation.lib.mjs';
38
+ import { classifyEntries, summarize, formatBytes, describeReason, buildActiveMatchers, DEFAULT_PROTECTED_NAMES, formatEntryContext, formatTaskSummary } from './cleanup.lib.mjs';
39
39
  import { getTempRoot, listTempEntries, getPathSize, readFolderGitInfo, listProcessHeldPaths, getActiveTasks, removePath, runSystemCleanup, collectProcessDebugReport, signalOrphanedAgentTrees } from './cleanup.os.lib.mjs';
40
40
  import { formatProcessDebugReport } from './process-debug.lib.mjs';
41
41
 
@@ -86,7 +86,7 @@ if (hasFlag('--version')) {
86
86
  }
87
87
 
88
88
  if (hasFlag('--help', '-h')) {
89
- console.log(`Usage: cleanup [options]
89
+ console.log(`Usage: hive-cleanup [options]
90
90
 
91
91
  Free disk space by removing stale hive-mind temporary directories/files while
92
92
  keeping folders that belong to active tasks and protected system paths.
@@ -244,8 +244,8 @@ async function main() {
244
244
  matchers = buildActiveMatchers(activeTasks);
245
245
  if (activeTasks.length > 0) {
246
246
  await log(`🏃 Active tasks detected: ${activeTasks.length}`);
247
- for (const t of activeTasks) {
248
- await log(` • ${t.owner}/${t.repo} ${t.type} #${t.number}${t.branch ? ` (branch ${t.branch})` : ''}`);
247
+ for (const task of activeTasks) {
248
+ await log(` • ${formatTaskSummary(task)}`);
249
249
  }
250
250
  } else {
251
251
  await log('🏃 No active tasks detected from running processes/sessions');
@@ -289,13 +289,13 @@ async function main() {
289
289
  await log('\n🟢 KEPT folders/files:');
290
290
  if (classified.keep.length === 0) await log(' (none)');
291
291
  for (const item of classified.keep.sort((a, b) => (b.size || 0) - (a.size || 0))) {
292
- await log(` ${formatBytes(item.size).padStart(7)} ${item.path} — ${describeReason(item.reason)}`);
292
+ await log(` ${formatBytes(item.size).padStart(7)} ${item.path} — ${describeReason(item.reason)}${formatEntryContext(item)}`);
293
293
  }
294
294
 
295
295
  await log(`\n🗑️ ${options.dryRun ? 'WOULD DELETE' : 'TO DELETE'} folders/files:`);
296
296
  if (classified.remove.length === 0) await log(' (none)');
297
297
  for (const item of classified.remove.sort((a, b) => (b.size || 0) - (a.size || 0))) {
298
- await log(` ${formatBytes(item.size).padStart(7)} ${item.path} — ${describeReason(item.reason)}`);
298
+ await log(` ${formatBytes(item.size).padStart(7)} ${item.path} — ${describeReason(item.reason)}${formatEntryContext(item)}`);
299
299
  }
300
300
 
301
301
  await log(`\n📊 Summary: keep ${totals.keepCount} (${formatBytes(totals.keepBytes)}), remove ${totals.removeCount} (${formatBytes(totals.removeBytes)})`);
@@ -309,15 +309,14 @@ async function main() {
309
309
  if (!options.force) {
310
310
  console.log(`\n⚠️ This will permanently delete ${classified.remove.length} entries (${formatBytes(totals.removeBytes)}).`);
311
311
  console.log('Type "yes" to confirm, or Ctrl+C to cancel:');
312
- process.stdout.write('> ');
313
312
  let answer = '';
314
313
  try {
315
- answer = execSync('read answer && echo $answer', { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'], shell: '/bin/bash' }).trim();
314
+ answer = await readConfirmationLine({ prompt: '> ' });
316
315
  } catch {
317
316
  await log('\n❌ Cancelled');
318
317
  return;
319
318
  }
320
- if (answer.toLowerCase() !== 'yes') {
319
+ if (!isConfirmationYes(answer)) {
321
320
  await log('\n❌ Cancelled');
322
321
  return;
323
322
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OS-interaction layer for the `cleanup` command (issue #1848).
2
+ * OS-interaction layer for the `hive-cleanup` command (issue #1848).
3
3
  *
4
4
  * Everything that touches the real filesystem, the process table (/proc),
5
5
  * isolation session state (`$ --status` from start-command), git metadata of a
@@ -625,7 +625,13 @@ export async function listActiveTaskRefsFromSessions(sessionIds) {
625
625
  const key = `${ref.owner}/${ref.repo}#${ref.number}:${ref.type}`;
626
626
  if (!seen.has(key)) {
627
627
  seen.add(key);
628
- refs.push(ref);
628
+ refs.push({
629
+ ...ref,
630
+ sessionId: status.uuid || id,
631
+ sessionName: status.sessionName || id,
632
+ status: status.status || null,
633
+ workspace: status.workingDirectory || null,
634
+ });
629
635
  }
630
636
  }
631
637
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared helpers for destructive CLI confirmations.
3
+ *
4
+ * @see https://github.com/link-assistant/hive-mind/issues/1930
5
+ */
6
+
7
+ import { createInterface } from 'node:readline/promises';
8
+ import { stdin, stdout } from 'node:process';
9
+
10
+ const ESC = String.fromCharCode(0x1b);
11
+ const BEL = String.fromCharCode(0x07);
12
+ const C1_CSI = String.fromCharCode(0x9b);
13
+
14
+ const ANSI_ESCAPE_PATTERN = new RegExp(`${ESC}(?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}]*(?:${BEL}|${ESC}\\\\)|[@-Z\\\\-_])`, 'g');
15
+ const C1_CSI_PATTERN = new RegExp(`${C1_CSI}[0-?]*[ -/]*[@-~]`, 'g');
16
+
17
+ function removeLastWord(chars) {
18
+ while (chars.length > 0 && /\s/.test(chars.at(-1))) chars.pop();
19
+ while (chars.length > 0 && !/\s/.test(chars.at(-1))) chars.pop();
20
+ }
21
+
22
+ /**
23
+ * Normalize an answer as it appeared after terminal line editing.
24
+ *
25
+ * Some terminal/window-manager shortcuts can inject escape sequences into the
26
+ * input stream even though the user still sees a clean `yes`. We strip those
27
+ * non-text sequences and replay common erase controls before comparing.
28
+ *
29
+ * @param {unknown} value
30
+ * @returns {string}
31
+ */
32
+ export function normalizeConfirmationInput(value) {
33
+ const raw = String(value ?? '')
34
+ .normalize('NFKC')
35
+ .replace(ANSI_ESCAPE_PATTERN, '')
36
+ .replace(C1_CSI_PATTERN, '');
37
+ const chars = [];
38
+
39
+ for (const char of raw) {
40
+ const code = char.codePointAt(0);
41
+
42
+ if (char === '\b' || char === '\u007f') {
43
+ chars.pop();
44
+ continue;
45
+ }
46
+
47
+ if (char === '\u0015') {
48
+ chars.length = 0;
49
+ continue;
50
+ }
51
+
52
+ if (char === '\u0017') {
53
+ removeLastWord(chars);
54
+ continue;
55
+ }
56
+
57
+ if (char === '\r' || char === '\n' || code === 0xfeff) continue;
58
+ if (code < 32 || (code >= 0x80 && code <= 0x9f)) continue;
59
+
60
+ chars.push(char);
61
+ }
62
+
63
+ return chars.join('').trim();
64
+ }
65
+
66
+ export function isConfirmationYes(value) {
67
+ return normalizeConfirmationInput(value).toLowerCase() === 'yes';
68
+ }
69
+
70
+ /**
71
+ * Read one interactive confirmation line.
72
+ *
73
+ * @param {{prompt?: string, input?: import('node:stream').Readable, output?: import('node:stream').Writable}} [options]
74
+ * @returns {Promise<string>}
75
+ */
76
+ export async function readConfirmationLine(options = {}) {
77
+ const input = options.input || stdin;
78
+ const output = options.output || stdout;
79
+ const rl = createInterface({
80
+ input,
81
+ output,
82
+ terminal: Boolean(input.isTTY && output.isTTY),
83
+ });
84
+
85
+ try {
86
+ return await rl.question(options.prompt ?? '> ');
87
+ } finally {
88
+ rl.close();
89
+ }
90
+ }
package/src/hive.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Import Sentry instrumentation first (must be before other imports)
3
3
  import './instrument.mjs';
4
- import { ensureUseM } from './use-m-bootstrap.lib.mjs';
4
+ import { ensureUseM, fetchUseMCodeFromCdn } from './use-m-bootstrap.lib.mjs';
5
5
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
6
6
  const earlyArgs = process.argv.slice(2);
7
7
  if (earlyArgs.includes('--version')) {
@@ -46,12 +46,7 @@ if (isRunningDirectly) {
46
46
  if (typeof globalThis.use === 'undefined') {
47
47
  try {
48
48
  await ensureUseM({
49
- fetchUseMCode: () =>
50
- withTimeout(
51
- fetch('https://unpkg.com/use-m/use.js').then(r => r.text()),
52
- 10000,
53
- 'fetching use-m library'
54
- ),
49
+ fetchUseMCode: () => withTimeout(fetchUseMCodeFromCdn(), 10000, 'fetching use-m library'),
55
50
  log: message => console.log(message),
56
51
  });
57
52
  } catch (error) {
@@ -48,6 +48,14 @@ export function isUsageLimitError(message) {
48
48
  // Provider-specific phrasings we've seen in the wild
49
49
  'session limit reached', // Claude
50
50
  'weekly limit reached', // Claude
51
+ // Issue #1935: Claude surfaces 5-hour / weekly account limits as
52
+ // "You've hit your session limit · resets 4pm (UTC)"
53
+ // "You've hit your weekly limit · resets Jan 15, 8am (UTC)"
54
+ // These arrive with api_error_status === 429 but are real usage limits with an
55
+ // explicit reset time, so detect the "hit your <window> limit" phrasing directly
56
+ // (independent of whether a parseable reset time is present in the message).
57
+ 'hit your session limit', // Claude 5-hour limit
58
+ 'hit your weekly limit', // Claude weekly limit
51
59
  'daily limit reached',
52
60
  'monthly limit reached',
53
61
  'billing hard limit',
@@ -1,19 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  export const USE_M_BOOTSTRAP_URL = 'https://unpkg.com/use-m/use.js';
4
- export const USE_M_FALLBACK_BOOTSTRAP_URL = 'https://unpkg.com/use-m@8.13.8/use.js';
4
+ export const USE_M_BOOTSTRAP_FALLBACK_URL = 'https://unpkg.com/use-m@8.13.8/use.js';
5
5
 
6
- const fetchUseMCodeFromUrl = async url => {
7
- const response = await fetch(url);
6
+ const isMissingUseMBundle = code => /^Not found: \/use-m@[^/]+\/use\.js\s*$/.test(code.trim());
7
+
8
+ const readBootstrapResponse = async (response, url) => {
8
9
  const code = await response.text();
9
- if (!response.ok || /^Not found:/i.test(code.trim())) {
10
- throw new Error(`failed to load use-m bootstrap from ${url}: ${response.status} ${response.statusText}`);
10
+ if (response.ok !== false && !isMissingUseMBundle(code)) return code;
11
+ throw new Error(`use-m bootstrap was not available at ${url}: ${code.slice(0, 120)}`);
12
+ };
13
+
14
+ const fetchUseMCodeFromUrl = async (url, fetcher = fetch) => readBootstrapResponse(await fetcher(url), url);
15
+
16
+ export const fetchUseMCodeFromCdn = async ({ fetcher = fetch } = {}) => {
17
+ let primaryError;
18
+ try {
19
+ return await fetchUseMCodeFromUrl(USE_M_BOOTSTRAP_URL, fetcher);
20
+ } catch (error) {
21
+ primaryError = error;
22
+ }
23
+
24
+ try {
25
+ return await fetchUseMCodeFromUrl(USE_M_BOOTSTRAP_FALLBACK_URL, fetcher);
26
+ } catch (fallbackError) {
27
+ throw new Error(`Failed to load use-m bootstrap from primary and fallback URLs: ${primaryError.message}; ${fallbackError.message}`);
11
28
  }
12
- return code;
13
29
  };
14
30
 
15
- const defaultFetchUseMCode = async () => fetchUseMCodeFromUrl(USE_M_BOOTSTRAP_URL);
16
- const fallbackFetchUseMCode = async () => fetchUseMCodeFromUrl(USE_M_FALLBACK_BOOTSTRAP_URL);
31
+ const defaultFetchUseMCode = () => fetchUseMCodeFromUrl(USE_M_BOOTSTRAP_URL);
32
+ const fallbackFetchUseMCode = () => fetchUseMCodeFromUrl(USE_M_BOOTSTRAP_FALLBACK_URL);
17
33
 
18
34
  /**
19
35
  * Load the shared use-m bootstrap.
@@ -28,7 +44,7 @@ export const ensureUseM = async (options = {}) => {
28
44
  try {
29
45
  globalThis.use = (await eval(await fetchUseMCode())).use;
30
46
  } catch (error) {
31
- if (typeof log === 'function') log(` use-m latest bootstrap failed (${error.message}); trying ${USE_M_FALLBACK_BOOTSTRAP_URL}`);
47
+ if (typeof log === 'function') log(` use-m latest bootstrap failed (${error.message}); trying ${USE_M_BOOTSTRAP_FALLBACK_URL}`);
32
48
  globalThis.use = (await eval(await fallbackFetchUseMCode())).use;
33
49
  }
34
50
  }