@link-assistant/hive-mind 2.15.2 → 2.17.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.hi.md +12 -0
  3. package/README.md +15 -0
  4. package/README.ru.md +15 -0
  5. package/README.zh.md +24 -12
  6. package/package.json +24 -17
  7. package/src/agent-snapshot-store.lib.mjs +252 -0
  8. package/src/agent.lib.mjs +25 -25
  9. package/src/agent.version-gates.lib.mjs +73 -0
  10. package/src/bot-lifecycle.lib.mjs +63 -5
  11. package/src/child-exit.lib.mjs +53 -1
  12. package/src/claude.session-tokens.lib.mjs +10 -8
  13. package/src/claude.session-transcript-repair.lib.mjs +65 -34
  14. package/src/cleanup.mjs +57 -3
  15. package/src/codex.lib.mjs +11 -1
  16. package/src/development-log.lib.mjs +22 -7
  17. package/src/disk-guard.lib.mjs +21 -1
  18. package/src/formal-ai-version.lib.mjs +10 -6
  19. package/src/github-error-reporter.lib.mjs +84 -2
  20. package/src/github.lib.mjs +212 -152
  21. package/src/instrument.mjs +12 -14
  22. package/src/instrument.sanitize.lib.mjs +52 -0
  23. package/src/isolation-runner.lib.mjs +56 -33
  24. package/src/isolation-runner.parsers.lib.mjs +29 -3
  25. package/src/isolation-runner.resume.lib.mjs +263 -0
  26. package/src/log-bounded-read.lib.mjs +411 -0
  27. package/src/log-sanitize-stream.lib.mjs +267 -0
  28. package/src/log-sanitize-worker-entry.mjs +31 -0
  29. package/src/log-sanitize-worker.lib.mjs +186 -0
  30. package/src/log-upload.lib.mjs +16 -4
  31. package/src/pull-request-changes.lib.mjs +1 -1
  32. package/src/session-completion-state.lib.mjs +124 -0
  33. package/src/session-kill-diagnostics.lib.mjs +117 -12
  34. package/src/session-kill-policy.lib.mjs +18 -7
  35. package/src/session-kill-resume.in-place.lib.mjs +136 -0
  36. package/src/session-kill-resume.lib.mjs +48 -18
  37. package/src/session-monitor.kill-sections.lib.mjs +8 -0
  38. package/src/session-monitor.lib.mjs +132 -9
  39. package/src/session-store.lib.mjs +15 -1
  40. package/src/solve.clone-errors.lib.mjs +86 -0
  41. package/src/solve.config.lib.mjs +5 -2
  42. package/src/solve.repository.lib.mjs +36 -63
  43. package/src/solve.resource-diagnostics.lib.mjs +105 -5
  44. package/src/start-command-cli.lib.mjs +60 -0
  45. package/src/telegram-bot.mjs +31 -91
  46. package/src/telegram-log-command.lib.mjs +7 -3
  47. package/src/telegram-overrides-validation.lib.mjs +73 -0
  48. package/src/telegram-terminal-watch-command.lib.mjs +9 -1
  49. package/src/working-session-summary.lib.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,130 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d37c412: Stop the unbounded agent snapshot leak: refuse pre-0.26.1 Agent CLIs, reclaim orphaned stores, and teach every disk check about `~/.local/share` (issue #2186).
8
+
9
+ One 9.5 h task left **115 directories / 31 GB** behind in `~/.local/share/link-assistant-agent/snapshot/`, growing at ~5 GB/h, while every disk check reported a healthy workspace. `@link-assistant/agent` keeps a rollback snapshot per project keyed on the worktree's _root commit_; Hive Mind runs each task in a throwaway checkout, so every run minted a new project, and up to agent 0.26.0 that store was a standalone git object database (no `objects/info/alternates`, ~270 MB each) that nothing ever removed. Hive Mind's own checks were blind to it: `disk-guard.lib.mjs` only looks at `/tmp`, cleanup only walks `/tmp` and `/var/tmp`, and the resource snapshots reported a flat `statfs` reading for `/`.
10
+
11
+ - **The leaking CLI is refused, not worked around.** `MIN_AGENT_SNAPSHOT_HYGIENE_VERSION = '0.26.1'` gates `validateAgentConnection` before any other probe, and both images pin `@link-assistant/agent@0.26.1` — the release that writes `objects/info/alternates` from `git rev-parse --git-path objects` and prunes projects whose worktree is gone (link-assistant/agent#298, PR #300). Verified by diffing the published 0.26.0 and 0.26.1 tarballs, not by reading a changelog.
12
+ - **Hive Mind reclaims orphans itself.** `src/agent-snapshot-store.lib.mjs` deletes a store only when the worktree recorded in `storage/project/<id>.json` no longer exists (or the record is gone) **and** the store has been idle for 15 minutes — agent's own `recentSnapshotAge`. The version floor alone would not be enough: the disk gate runs before agent does, `--tool codex` / `--tool claude` tasks never run agent at all, and a host upgraded from 0.26.0 still carries what it already leaked.
13
+ - **It runs unconditionally at the end of a task**, not only under `--auto-cleanup`: an orphaned store has no worktree left to restore into, so it has no debugging value.
14
+ - **The disk model knows about agent state.** The pre-flight disk guard reclaims orphaned stores _before_ solver workspaces (an orphan is pure garbage, a workspace may still be wanted), `hive-cleanup` gained an agent-snapshot category with `--dry-run` reporting and a `--no-agent-snapshots` opt-out, and the `📈 Resource usage` block now reports the store count and size, so `after_agent` shows this growth instead of a flat `/` reading. The size walk is bounded so measuring a 31 GB tree cannot become the next incident, and the resource markers only carry the new fields when there is agent state to report, so existing markers parse unchanged.
15
+
16
+ The rest of the stack moves with it: Formal AI 0.339.1 → 0.345.0, Rust 1.96 → 1.98 for the Formal AI builder, `konard/box` and `konard/box-dind` 2.3.5 → 2.4.0, `actions/checkout` and `actions/setup-node` v6 → v7, `@changesets/cli` 3.0.1 → 3.0.2. The Formal AI bump was checked the same way as the agent one: 0.345.0's `Cargo.lock` still carries no `openssl-sys` (so the builder keeps working on a stock `rust:slim` image) and its four memory-contract sources are byte-identical to 0.339.1, so the `memory upgrade-status` / `memory migrate` contract the container updater depends on is unchanged.
17
+
18
+ Evidence, reproduction scripts and the full analysis are in `docs/case-studies/issue-2186/README.md` and `experiments/issue-2186/`.
19
+
20
+ - 5e918d5: Fix the release failure and the CI/CD gaps behind issue #2198.
21
+
22
+ - `changeset version` no longer resolves `bun` from a stale root `bun.lock`:
23
+ the lockfile is removed and `devEngines.packageManager` declares npm, so
24
+ `@changesets/format` cannot spawn a package manager the runner does not have.
25
+ - New `scripts/check-package-manager.mjs` guard, run in the lint job, fails
26
+ loudly if a foreign lockfile or a missing package manager declaration comes
27
+ back.
28
+ - Lockfile changes now count as package changes in `detect-code-changes.mjs`,
29
+ so the guard cannot be gated out.
30
+ - Every workflow now declares the narrowest default `permissions` that still
31
+ allows checkout, instead of `read-all`, clearing zizmor's
32
+ `excessive-permissions` findings.
33
+ - secretlint was installed on every build but never run as a linter and had no
34
+ config; `npm run check:secrets` now runs it in the lint job, fail-closed,
35
+ with a narrow `.secretlintignore` for the fixtures that hold fake secrets on
36
+ purpose.
37
+ - A Broken Link Checker workflow (lychee, with a Wayback Machine fallback) and
38
+ an offline relative-link test now cover documentation links. They catch the
39
+ `docs/FREE_MODELS.md` case-study link that had pointed at a file which never
40
+ existed in any commit, in all four translations.
41
+ - All eight Docker jobs booted buildx through a bare
42
+ `docker/setup-buildx-action`, so a transient registry outage failed the
43
+ publish; they now go through a `setup-buildx-resilient` composite action that
44
+ pre-pulls the pinned BuildKit image with backoff and falls back to a
45
+ pull-through mirror.
46
+ - Nothing audited the dependency tree: CodeQL analyses our own source, and
47
+ `dependency-review-action` only runs on pull requests and only inspects the
48
+ dependencies a PR changes, so an advisory against a long-pinned package was
49
+ invisible to both. `security.yml` gains an `npm-audit` job running
50
+ `npm audit --package-lock-only --audit-level=high`, which also runs on the
51
+ existing schedule.
52
+ - actionlint is pinned to 1.7.12 instead of 1.7.7.
53
+
54
+ - Eleven links pointed at this repository under its former owner
55
+ (`deep-assistant/hive-mind`). The GitHub API reports that name as
56
+ `301 Moved Permanently`, but the pages a reader actually clicks answer 404,
57
+ so every one of them was broken. All rewritten, including six in a directory
58
+ the link checker excludes and would never have reported. The suppression list
59
+ it needed alongside them was also wrong: it carried a `www.npmjs.com` pattern
60
+ while the README links the bare host, so the rule matched nothing. Both are
61
+ now asserted — the guard checks that each suppressed URL is genuinely
62
+ _matched_ by a pattern, not merely mentioned near one.
63
+ - Kept the 1350-line warning threshold met after merging `main`: issue #2186 grew
64
+ `src/agent.lib.mjs` to 1357 lines, so the Agent CLI version floors moved to
65
+ `src/agent.version-gates.lib.mjs` (re-exported, so no importer changed).
66
+ - The change detector no longer trusts a PR head that was never tested. Issue
67
+ #1665's incremental `before..after` diff is correct only if the previous head
68
+ passed, and `cancel-in-progress` means it often had not: a docs commit pushed
69
+ minutes after a code commit cancels that commit's run and then skips the code
70
+ jobs itself, leaving the branch green over code no job ever ran.
71
+ `detect-code-changes.mjs` now asks the Actions API whether a successful run is
72
+ recorded for the previous head, and widens to the full PR diff whenever the
73
+ answer is anything else — including when the lookup itself fails, since an
74
+ unanswerable lookup must not be read as a "yes".
75
+ - F16: the F4 test matched npm's `allow-scripts` log prefix verbatim, so npm 11.19
76
+ renaming it to `install-scripts` turned `test-suites` red under a message claiming
77
+ npm had fixed the underlying bug. It had not — `linkPkg()` still resolves no
78
+ allowScripts policy, and `--ignore-scripts` is still the only lever that works.
79
+ The check now keys on the sentence that states the defect, pins both shipped
80
+ labels as samples, and no longer asserts a cause it cannot distinguish.
81
+ - F17: CodeQL flagged two of this PR's own test assertions as incomplete URL
82
+ sanitization. The alert was wrong about the security property — the flagged value
83
+ is a log the test's own mock wrote, not a URL anyone trusts — and right about the
84
+ code: `calls.includes('mirror.gcr.io')` was never what "the mirror was contacted"
85
+ meant. The assertions now compare registry hosts by equality, which retires both
86
+ alerts at the source and, being exact, immediately caught a retry budget no test
87
+ had been asserting.
88
+
89
+ - ce43cc9: Resume a killed session in the container it died in, reconcile executions that outlived their supervisor, and move the whole dependency set forward (issue #2189).
90
+
91
+ The first half of #2189 shipped in 2.16.0 with one requirement left open: R2 asked for a killed session to be continued **in the same `$` session / container**, and `$` could not do that yet. The three upstream issues this repository filed — `link-foundation/start#162` (`--resume`), `#164` (argv flattened with `join(' ')`), `#165` (a V8 self-abort reported as `oomKilled=false`) — are delivered in `start-command@0.33.0`, so R2 is closed here and the rest of the toolchain is brought up with it.
92
+
93
+ - **Same-container resume.** `src/isolation-runner.resume.lib.mjs` wraps `$ --resume <id> [-- <command>]`, and `src/session-kill-resume.in-place.lib.mjs` prefers it over a fresh isolated run: the clone, the caches and the half-finished branch survive the kill instead of being rebuilt from scratch. When in-place resume is unavailable or refused, the previous behaviour — a new isolated session — still runs, and the reason is recorded on the session record (`killRecoveryInPlace`, `killRecoveryResumeMode`) and in the operator's report.
94
+ - **No more limbo executions.** The bot reconciles at startup with `$ --resume-all`: a container still running is re-attached to, and one that ended while unsupervised is finalized with the exit code it actually had, rather than being polled forever as "executing".
95
+ - **`$` refusals are read as refusals.** `command-stream`'s `$` _resolves_ on a non-zero exit instead of throwing, so `$ --stop <unknown>` ("No execution found…", exit 1) had been reported to the operator as a stop that happened. Every `$` invocation now inspects the exit code, and both resume wrappers additionally distinguish an older `$` that does not know the verb (`unsupported: true`, so callers keep their old path) from a genuine refusal.
96
+ - **0.33.0's kill hints are consumed.** `exitReason` and `memoryExhausted` from `$ --status` feed `describeKillCause` directly, so a runtime self-abort is named as out of memory from the supervisor's own answer rather than only from log forensics.
97
+ - **The dependency set is current, and what changed in it is used.** `@changesets/cli` 2 → 3, `jscpd` 4 → 5, `@sentry/node` 10.62 → 10.73, `prettier` 3.8.5 → 3.9.6, `agent-commander` 0.8 → 0.10, `dayjs` 1.11.21 → 1.11.23, `eslint` 10.5 → 10.9, `lint-staged` 17.0 → 17.4, secretlint 13.0.2 → 13.0.5, `lino-objects-codec` 0.4 → 0.8, `lino-i18n` 0.1 → 0.2.
98
+
99
+ Three of those bumps were behaviour changes that needed work rather than a version number:
100
+
101
+ - `changeset version` **exits 1** when there are no unreleased changesets in 3.0, where 2.x warned and exited 0. The release job reads the changeset count before rebasing onto the remote, and the rebase can consume the very changesets the decision was made on — a race that turned into a red release. `versionAndCommit` re-reads the count after the rebase and reports the already-published version through the existing `already_released` path.
102
+ - `jscpd` 5 is a Rust rewrite that treats `skipComments` as an **unknown field**: it warned and silently fell back to `mode: mild`, quietly loosening the duplication gate. `.jscpd.json` now says `"mode": "weak"`, which is what that option means in 5.
103
+ - `@sentry/node` 10.71+ turns structured logs on by default, and they bypass `beforeSend` entirely. The redaction this repository relies on was therefore no longer covering everything it left through; `src/instrument.sanitize.lib.mjs` is now shared between `beforeSend` and a new `beforeSendLog` hook, so both surfaces are scrubbed by the same code.
104
+
105
+ `node-pty` (pulled in by `agent-commander` 0.10's real-TUI capture) is denied a native build in `allowScripts`: it runs in a separate spawned pty host that this repository never launches headlessly.
106
+
107
+ Evidence for every claim above — including the two dependency behaviours that were checked and found _not_ to affect this repository — is in `docs/case-studies/issue-2189/README.md` under "Dependency Follow-Through", with the reproduction scripts in `experiments/issue-2189/`.
108
+
109
+ ## 2.16.0
110
+
111
+ ### Minor Changes
112
+
113
+ - 7fe1b88: Stop Hive Mind from running itself out of memory while reporting a finished session, classify a runtime self-abort as what it is, and resume the session instead of offering to (issue #2189).
114
+
115
+ A `solve` run finished successfully, pushed its work and converted its pull request to ready — then died ten minutes later inside Hive Mind's own `--attach-logs` step with `FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory`, on a machine with 10.3 GB of RAM free. It was reported to the operator as a "forced kill", six hours late, and was never continued. Six changes close that path:
116
+
117
+ - **The sanitizer streams.** `--attach-logs` read a 134 MB log into one JS string, sanitized it (full copy), escaped it (full copy), built a comment it was never going to post, then sanitized the raw content again for the upload. `src/log-sanitize-stream.lib.mjs` sanitizes a log file into a destination file 1 MiB at a time, holding back only what could still be part of a token — a partial line, an unterminated PEM block, a trailing wrapped-base64 run — with a hard cap on the hold. `attachLogToGitHub` now picks the publication route from `logStats.size` **before** reading anything, and the content is sanitized exactly once.
118
+ - **A heap-capped worker behind it.** `sanitizeLogFileToFileBounded` routes logs at or above 16 MiB into a `worker_threads` worker with `resourceLimits.maxOldGenerationSizeMb = 512`, so a residual unbounded allocation costs one thread and one failed log upload rather than the working session. All four whole-file sanitize sites use it: the `--attach-logs` upload, the `/log` Telegram command, the GitHub error reporter and development-log collection.
119
+ - **A V8 self-abort is out of memory.** `docker inspect` said `OOMKilled=false` and cgroup `memory.events` said `oom_kill 0` — both correct, and both blind to a process that stops at its own ~2.1 GB heap cap. `FATAL_MEMORY_PATTERNS` / `findFatalMemoryMarker` recognise the fatal lines Node/V8, Rust, Go and C++ print on their way out, and an abnormal exit whose log carries one is `KILL_CAUSE_OUT_OF_MEMORY` regardless of cgroup counters. A clean exit is never upgraded, so a log that merely quotes the phrase cannot manufacture a diagnosis.
120
+ - **`--on-session-kill` defaults to `resume`.** A killed session with a recoverable tool session id now continues on its own instead of waiting for a human to paste a command; `report` and `HIVE_MIND_ON_SESSION_KILL` still opt out, and `--session-kill-resume-attempts` bounds it.
121
+ - **A killed session reaches a handled state.** The same dead session was re-detected on every poll — re-resolving the pull request, re-scanning the 134 MB log and re-sending the notification each cycle, with bot RSS climbing 1.78 → 1.84 GB against a ~2 GB cap. `src/session-completion-state.lib.mjs` adds a persisted handled latch and caches the recovered tool session id on the session record, so the log is scanned at most once and per-cycle work is no longer O(log size).
122
+ - **No unbounded log read is left.** Every artifact whose size is decided by how long an AI ran — `/log`, `/terminal_watch`, development-log collection, the error reporter, Claude session accounting and transcript repair, Codex's last-message artifact, and every session-monitor read — is streamed or reads a bounded head/tail. The `require-sanitized-output` ESLint rule learns the streaming sanitizers so a streamed artifact still counts as provably sanitized.
123
+
124
+ Telemetry was extended so the next incident is diagnosable: resource snapshots now carry the V8 heap used, total, external and `heap_size_limit` with the used share of it, warn at 85%, and `attachLogToGitHub` brackets itself with `log_upload_start` / `log_upload_end` samples. `describeKillCause` classifies an abnormal exit with a heap at or above 90% of its limit as out of memory even when the fatal line was lost with the truncated tail.
125
+
126
+ Full analysis — timeline, root causes, and the requirement-by-requirement plan — is in `docs/case-studies/issue-2189/`. Two upstream issues were filed from it: `link-foundation/start#165` (a V8 self-abort is reported as `oomKilled=false` with no memory signal in `--status`) and `link-foundation/start#164` (command argv is flattened with `join(' ')`, so quoted arguments are re-parsed by the inner shell).
127
+
3
128
  ## 2.15.2
4
129
 
5
130
  ### Patch Changes
package/README.hi.md CHANGED
@@ -797,6 +797,18 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
797
797
  (cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
798
798
  ```
799
799
 
800
+ **मारे गए सत्र स्वयं को पुनर्प्राप्त करते हैं।** `--on-session-kill=resume` (डिफ़ॉल्ट) के साथ, आउट-ऑफ़-मेमोरी
801
+ किलर, भरी हुई डिस्क या फोर्स्ड किल से मारा गया कार्य सत्र स्वचालित रूप से पुनः आरंभ होता है, जिसकी सीमा
802
+ `--session-kill-resume-attempts` तय करती है। जब आइसोलेशन बैकएंड अनुमति देता है और `$`
803
+ (`start-command`) कम से कम 0.33.0 है, तब रिकवरी सत्र शून्य से शुरू करने के बजाय **उसी कंटेनर में
804
+ फिर से प्रवेश करता है**, इसलिए क्लोन, कैश और अधूरी ब्रांच सुरक्षित रहते हैं।
805
+
806
+ **बॉट के पुनः आरंभ से चालू काम अनाथ नहीं होता।** शुरू होते समय Telegram बॉट आइसोलेशन बैकएंड के अपने
807
+ चालू निष्पादनों के दृश्य से मिलान करता है (`$ --resume-all`): जो अभी जीवित है उसे उसका कंप्लीशन वॉचर
808
+ वापस मिल जाता है, और जो बिना निगरानी के समाप्त हो गया उसे अंतिम रूप देकर रिपोर्ट किया जाता है।
809
+ `--on-session-kill` और `--session-kill-resume-attempts` के लिए
810
+ [docs/CONFIGURATION.hi.md](./docs/CONFIGURATION.hi.md) देखें।
811
+
800
812
  ### डिस्क क्लीनअप
801
813
 
802
814
  `hive-cleanup` पुरानी hive-mind अस्थायी डायरेक्टरी/फ़ाइलों (जैसे प्रति-कार्य क्लोन
package/README.md CHANGED
@@ -815,6 +815,21 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
815
815
  (cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
816
816
  ```
817
817
 
818
+ **Killed sessions recover themselves.** With `--on-session-kill=resume` (the
819
+ default) a working session killed by the out-of-memory killer, a full disk or a
820
+ forced kill is restarted automatically, bounded by
821
+ `--session-kill-resume-attempts`. When the isolation backend allows it and `$`
822
+ (`start-command`) is at least 0.33.0, the recovery session **re-enters the same
823
+ container** instead of starting from scratch, so the clone, the caches and the
824
+ half-finished branch survive.
825
+
826
+ **A bot restart never orphans in-flight work.** On startup the Telegram bot
827
+ reconciles the isolation backend's own view of still-running executions
828
+ (`$ --resume-all`): what is still alive gets its completion watcher back, what
829
+ died unsupervised is finalized and reported. See
830
+ [docs/CONFIGURATION.md](./docs/CONFIGURATION.md) for `--on-session-kill` and
831
+ `--session-kill-resume-attempts`.
832
+
818
833
  ### Disk Cleanup
819
834
 
820
835
  `hive-cleanup` frees disk space by removing stale hive-mind temporary
package/README.ru.md CHANGED
@@ -798,6 +798,21 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
798
798
  (cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
799
799
  ```
800
800
 
801
+ **Убитые сессии восстанавливаются сами.** При `--on-session-kill=resume` (по
802
+ умолчанию) рабочая сессия, убитая OOM-киллером, переполненным диском или
803
+ принудительным завершением, перезапускается автоматически — в пределах
804
+ `--session-kill-resume-attempts`. Если это позволяет бэкенд изоляции, а `$`
805
+ (`start-command`) не ниже 0.33.0, восстановительная сессия **входит в тот же
806
+ контейнер**, а не начинает с нуля: клон, кэши и незавершённая ветка остаются на
807
+ месте.
808
+
809
+ **Перезапуск бота не бросает работу на полпути.** При старте Telegram-бот
810
+ сверяет собственный список выполняющихся запусков бэкенда изоляции
811
+ (`$ --resume-all`): к живым возвращается наблюдатель за завершением, а те, что
812
+ умерли без присмотра, финализируются и попадают в отчёт. Параметры
813
+ `--on-session-kill` и `--session-kill-resume-attempts` описаны в
814
+ [docs/CONFIGURATION.ru.md](./docs/CONFIGURATION.ru.md).
815
+
801
816
  ### Очистка диска
802
817
 
803
818
  `hive-cleanup` освобождает место на диске, удаляя устаревшие временные каталоги/файлы
package/README.zh.md CHANGED
@@ -21,18 +21,18 @@
21
21
 
22
22
  Hive Mind 是一款**通用 AI**(迷你 AGI),能够处理广泛的任务,不仅限于编程。几乎所有可以通过操作仓库文件完成的事情,都可以实现自动化。
23
23
 
24
- | 特性 | 对您的意义 |
25
- | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
26
- | **无需全程盯守** | 完全自主模式,拥有 sudo 权限。AI 享有像真实程序员一样的创造自由。 |
27
- | **云端隔离** | 运行在专用虚拟机或 Docker 上,出现问题易于恢复。 |
28
- | **完整互联网 + Sudo 权限** | AI 可以按需安装软件包、获取文档并配置系统。 |
29
- | **预装工具链** | 25GB+ 开箱即用:10 种语言运行时、2 个定理证明器、构建工具,还可继续安装更多。 |
30
- | **高效利用 Token** | 常规任务通过代码自动化完成,让 AI Token 专注于创造性问题解决。 |
31
- | **节省时间** | 人类需要 2~8 小时的工作,AI 每个工作会话仅需 10~25 分钟完成。可批量执行仓库中的任务。"代码在你睡觉时已写好。" |
32
- | **编排式扩展** | 并行工作进程犹如一支开发团队。可将 Claude MAX 和 ChatGPT Pro(各 $200)配对,获得两份独立的近乎无限预算。 |
33
- | **人工控制** | AI 创建草稿 PR——由您决定是否合并。在关键节点设置质量把关。 |
34
- | **任意设备编程** | 通过 Telegram 机器人使用 `/solve` 和 `/hive` 命令,在任意设备上管理 AI,无需 PC、IDE 或笔记本电脑。 |
35
- | **100% 开源** | Unlicense(公共领域)。完全透明,无供应商锁定。 |
24
+ | 特性 | 对您的意义 |
25
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------- |
26
+ | **无需全程盯守** | 完全自主模式,拥有 sudo 权限。AI 享有像真实程序员一样的创造自由。 |
27
+ | **云端隔离** | 运行在专用虚拟机或 Docker 上,出现问题易于恢复。 |
28
+ | **完整互联网 + Sudo 权限** | AI 可以按需安装软件包、获取文档并配置系统。 |
29
+ | **预装工具链** | 25GB+ 开箱即用:10 种语言运行时、2 个定理证明器、构建工具,还可继续安装更多。 |
30
+ | **高效利用 Token** | 常规任务通过代码自动化完成,让 AI Token 专注于创造性问题解决。 |
31
+ | **节省时间** | 人类需要 28 小时的工作,AI 每个工作会话仅需 1025 分钟完成。可批量执行仓库中的任务。"代码在你睡觉时已写好。" |
32
+ | **编排式扩展** | 并行工作进程犹如一支开发团队。可将 Claude MAX 和 ChatGPT Pro(各 $200)配对,获得两份独立的近乎无限预算。 |
33
+ | **人工控制** | AI 创建草稿 PR——由您决定是否合并。在关键节点设置质量把关。 |
34
+ | **任意设备编程** | 通过 Telegram 机器人使用 `/solve` 和 `/hive` 命令,在任意设备上管理 AI,无需 PC、IDE 或笔记本电脑。 |
35
+ | **100% 开源** | Unlicense(公共领域)。完全透明,无供应商锁定。 |
36
36
 
37
37
  **费用**:Hive Mind 支持两种 $200/月订阅作为功能完整、近乎“无限”的选项:
38
38
 
@@ -788,6 +788,18 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
788
788
  (cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
789
789
  ```
790
790
 
791
+ **被杀死的会话会自我恢复。** 使用 `--on-session-kill=resume`(默认值)时,被
792
+ OOM killer、磁盘写满或强制终止杀死的工作会话会自动重启,次数受
793
+ `--session-kill-resume-attempts` 限制。当隔离后端允许且 `$`(`start-command`)
794
+ 不低于 0.33.0 时,恢复会话会**重新进入同一个容器**,而不是从零开始,因此克隆、
795
+ 缓存和未完成的分支都得以保留。
796
+
797
+ **重启机器人不会遗弃进行中的工作。** Telegram 机器人启动时会与隔离后端自身记录的
798
+ 运行中执行进行对账(`$ --resume-all`):仍存活的会重新挂上完成监视器,在无人监管
799
+ 时已经结束的则被最终确认并上报。`--on-session-kill` 与
800
+ `--session-kill-resume-attempts` 的说明参见
801
+ [docs/CONFIGURATION.zh.md](./docs/CONFIGURATION.zh.md)。
802
+
791
803
  ### 磁盘清理
792
804
 
793
805
  `hive-cleanup` 通过删除过时的 hive-mind 临时目录/文件(如每个任务的克隆
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.15.2",
3
+ "version": "2.17.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -24,12 +24,13 @@
24
24
  "lint": "eslint 'src/**/*.{js,mjs,cjs}' 'scripts/**/*.{js,mjs,cjs}' 'eslint-rules/**/*.{js,mjs,cjs}' 'tests/**/*.{js,mjs,cjs}'",
25
25
  "lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' 'scripts/**/*.{js,mjs,cjs}' 'eslint-rules/**/*.{js,mjs,cjs}' 'tests/**/*.{js,mjs,cjs}' --fix",
26
26
  "check:duplication": "jscpd .",
27
+ "check:secrets": "secretlint --secretlintignore .secretlintignore \"**/*\"",
27
28
  "format": "prettier --write \"**/*.{js,mjs,json,md}\" --ignore-path .prettierignore",
28
29
  "format:check": "prettier --check \"**/*.{js,mjs,json,md}\" --ignore-path .prettierignore",
29
30
  "changeset": "changeset",
30
31
  "changeset:version": "changeset version",
31
32
  "changeset:publish": "npm run build:pre && changeset publish",
32
- "build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/fix.mjs && chmod +x src/configure-claude.mjs && chmod +x src/hive-screens.mjs",
33
+ "build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/task.mjs && chmod +x src/fix.mjs && chmod +x src/cleanup.mjs && chmod +x src/review.mjs && chmod +x src/configure-claude.mjs && chmod +x src/start-screen.mjs && chmod +x src/hive-screens.mjs && chmod +x src/telegram-bot.mjs",
33
34
  "prepare": "husky"
34
35
  },
35
36
  "repository": {
@@ -53,34 +54,39 @@
53
54
  "engines": {
54
55
  "node": ">=24.0.0"
55
56
  },
57
+ "devEngines": {
58
+ "packageManager": {
59
+ "name": "npm"
60
+ }
61
+ },
56
62
  "files": [
57
63
  "src",
58
64
  "*.md"
59
65
  ],
60
66
  "devDependencies": {
61
- "@changesets/cli": "^2.31.0",
67
+ "@changesets/cli": "^3.0.2",
62
68
  "@eslint/js": "^10.0.1",
63
- "eslint": "^10.5.0",
69
+ "eslint": "^10.9.1",
64
70
  "eslint-config-prettier": "^10.1.8",
65
71
  "eslint-plugin-prettier": "^5.5.6",
66
72
  "husky": "^9.1.7",
67
- "jscpd": "^4.0.5",
68
- "lint-staged": "^17.0.8",
69
- "prettier": "^3.8.5",
73
+ "jscpd": "^5.1.2",
74
+ "lint-staged": "^17.4.1",
75
+ "prettier": "^3.9.6",
70
76
  "test-anywhere": "^0.9.1"
71
77
  },
72
78
  "dependencies": {
73
- "@secretlint/core": "^13.0.2",
74
- "@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
75
- "@sentry/node": "^10.62.0",
76
- "@sentry/profiling-node": "^10.62.0",
77
- "agent-commander": "^0.8.0",
78
- "dayjs": "^1.11.21",
79
+ "@secretlint/core": "^13.0.5",
80
+ "@secretlint/secretlint-rule-preset-recommend": "^13.0.5",
81
+ "@sentry/node": "^10.73.0",
82
+ "@sentry/profiling-node": "^10.73.0",
83
+ "agent-commander": "^0.10.1",
84
+ "dayjs": "^1.11.23",
79
85
  "decimal.js-light": "^2.5.1",
80
86
  "lino-arguments": "^0.3.0",
81
- "lino-i18n": "^0.1.1",
82
- "lino-objects-codec": "^0.4.0",
83
- "secretlint": "^13.0.2",
87
+ "lino-i18n": "^0.2.0",
88
+ "lino-objects-codec": "^0.8.0",
89
+ "secretlint": "^13.0.5",
84
90
  "semver": "^7.8.5",
85
91
  "tinyld": "^1.3.4"
86
92
  },
@@ -90,6 +96,7 @@
90
96
  ]
91
97
  },
92
98
  "allowScripts": {
93
- "@sentry/node-cpu-profiler@2.4.2": true
99
+ "@sentry/node-cpu-profiler@2.4.3": true,
100
+ "node-pty@1.2.0-beta.15": false
94
101
  }
95
102
  }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Reclaim orphaned `@link-assistant/agent` snapshot stores (issue #2186).
3
+ *
4
+ * Agent keeps a rollback snapshot per project in
5
+ * `$XDG_DATA_HOME/link-assistant-agent/snapshot/<project id>`, where the project
6
+ * id is the worktree's *root commit*, and records the worktree it belongs to in
7
+ * `storage/project/<project id>.json`. Hive Mind runs its tools in throwaway
8
+ * checkouts, so every fresh `git init` looks like a brand-new project and gets
9
+ * its own store; up to agent 0.26.0 that store was a standalone object database
10
+ * (no `objects/info/alternates`) that nothing ever removed. One 9.5 h task left
11
+ * 115 stores / 31 GB behind, ~270 MB each, at ~5 GB/h.
12
+ *
13
+ * Agent 0.26.1 fixes both halves upstream — it shares the repository's objects
14
+ * and prunes dead projects itself (link-assistant/agent#298) — and
15
+ * `src/agent.lib.mjs` refuses to run on anything older. That is not the whole
16
+ * story for Hive Mind, for three reasons:
17
+ *
18
+ * - agent only prunes when agent runs, and the disk gate that decides whether
19
+ * a task may start at all runs *before* that (and before any `--tool codex`
20
+ * / `--tool claude` task, which never runs agent);
21
+ * - a host upgraded from 0.26.0 still carries whatever it leaked before;
22
+ * - `disk-guard.lib.mjs`, `cleanup.lib.mjs` and the 10 GB pre-flight gate only
23
+ * ever looked at `/tmp`, so this growth was invisible to every disk check
24
+ * Hive Mind owns — the exact blind spot issue #2186 reported.
25
+ *
26
+ * The pruning rule is the conservative one from the issue: a store is garbage
27
+ * only when the worktree its project record points at no longer exists (or the
28
+ * record is gone entirely) *and* the store has been idle for `minIdleMs`. A
29
+ * store whose worktree is still on disk belongs to a live checkout and is never
30
+ * touched, so this cannot interfere with a concurrent session. Orphans have no
31
+ * debugging value either — there is nothing left to restore them into — so this
32
+ * is not gated on `--auto-cleanup`.
33
+ *
34
+ * Every side effect (readdir, stat, rm, clock) is injectable so the behaviour is
35
+ * unit-testable without a real agent installation.
36
+ *
37
+ * @see https://github.com/link-assistant/hive-mind/issues/2186
38
+ * @see https://github.com/link-assistant/agent/issues/298
39
+ */
40
+
41
+ import fsPromises from 'node:fs/promises';
42
+ import os from 'node:os';
43
+ import path from 'node:path';
44
+
45
+ /** Directory agent creates under the XDG data home. */
46
+ export const AGENT_DATA_DIR_NAME = 'link-assistant-agent';
47
+
48
+ /**
49
+ * A store modified this recently is never reclaimed. 15 minutes is agent's own
50
+ * `recentSnapshotAge` (src/project/project.ts), which is what keeps a store that
51
+ * was just created — before its project record has been written — from being
52
+ * mistaken for an orphan.
53
+ */
54
+ export const DEFAULT_AGENT_SNAPSHOT_MIN_IDLE_MS = 15 * 60 * 1000;
55
+
56
+ /** `$XDG_DATA_HOME/link-assistant-agent`, or `~/.local/share/link-assistant-agent`. */
57
+ export const getAgentDataHome = ({ env = process.env, homeDir = os.homedir } = {}) => {
58
+ const xdgDataHome = String(env?.XDG_DATA_HOME || '').trim();
59
+ const dataRoot = xdgDataHome || path.join(homeDir(), '.local', 'share');
60
+ return path.join(dataRoot, AGENT_DATA_DIR_NAME);
61
+ };
62
+
63
+ /** Every `snapshot/<project id>` directory, oldest modification first. */
64
+ export const listAgentSnapshotStores = async ({ dataHome = getAgentDataHome(), fileSystem = fsPromises } = {}) => {
65
+ const snapshotRoot = path.join(dataHome, 'snapshot');
66
+ let names;
67
+ try {
68
+ names = await fileSystem.readdir(snapshotRoot);
69
+ } catch {
70
+ return [];
71
+ }
72
+ const stores = [];
73
+ for (const entry of names) {
74
+ const name = typeof entry === 'string' ? entry : entry?.name;
75
+ if (!name) continue;
76
+ const storePath = path.join(snapshotRoot, name);
77
+ try {
78
+ const stats = await fileSystem.stat(storePath);
79
+ if (!stats.isDirectory()) continue;
80
+ stores.push({ id: name, path: storePath, mtimeMs: Number(stats.mtimeMs) || 0 });
81
+ } catch {
82
+ continue;
83
+ }
84
+ }
85
+ return stores.sort((a, b) => a.mtimeMs - b.mtimeMs);
86
+ };
87
+
88
+ /**
89
+ * The worktree agent recorded for a project id, `null` when there is no usable
90
+ * record. A missing record is not an error: agent writes the store first.
91
+ */
92
+ export const readAgentProjectWorktree = async ({ dataHome = getAgentDataHome(), projectId, fileSystem = fsPromises } = {}) => {
93
+ try {
94
+ const raw = await fileSystem.readFile(path.join(dataHome, 'storage', 'project', `${projectId}.json`), 'utf8');
95
+ const worktree = JSON.parse(String(raw))?.worktree;
96
+ return typeof worktree === 'string' && worktree ? worktree : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ };
101
+
102
+ /** True when `worktree` still exists as a directory. */
103
+ const isWorktreeAlive = async (worktree, fileSystem) => {
104
+ if (!worktree) return false;
105
+ try {
106
+ return (await fileSystem.stat(worktree)).isDirectory();
107
+ } catch {
108
+ return false;
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Split the stores into the ones that are safe to delete and the ones that must
114
+ * stay, with the reason for each so `hive-cleanup --dry-run` can explain itself.
115
+ *
116
+ * @returns {Promise<{orphaned: Array<{id: string, path: string, mtimeMs: number, worktree: string|null, reason: string}>, keep: Array<{id: string, path: string, mtimeMs: number, worktree: string|null, reason: string}>}>}
117
+ */
118
+ export const classifyAgentSnapshotStores = async ({ dataHome = getAgentDataHome(), minIdleMs = DEFAULT_AGENT_SNAPSHOT_MIN_IDLE_MS, now = Date.now, fileSystem = fsPromises, stores = null } = {}) => {
119
+ const candidates = stores || (await listAgentSnapshotStores({ dataHome, fileSystem }));
120
+ const currentTime = now();
121
+ const orphaned = [];
122
+ const keep = [];
123
+ for (const store of candidates) {
124
+ const worktree = await readAgentProjectWorktree({ dataHome, projectId: store.id, fileSystem });
125
+ if (await isWorktreeAlive(worktree, fileSystem)) {
126
+ keep.push({ ...store, worktree, reason: 'worktree_alive' });
127
+ continue;
128
+ }
129
+ if (currentTime - store.mtimeMs < minIdleMs) {
130
+ keep.push({ ...store, worktree, reason: 'recently_modified' });
131
+ continue;
132
+ }
133
+ orphaned.push({ ...store, worktree, reason: worktree ? 'worktree_gone' : 'no_project_record' });
134
+ }
135
+ return { orphaned, keep };
136
+ };
137
+
138
+ /** Human-readable form of the reasons produced by {@link classifyAgentSnapshotStores}. */
139
+ export const describeAgentSnapshotReason = reason =>
140
+ ({
141
+ worktree_alive: 'worktree still exists',
142
+ recently_modified: 'modified too recently to be considered idle',
143
+ worktree_gone: 'recorded worktree no longer exists',
144
+ no_project_record: 'no project record, idle',
145
+ remove_failed: 'could not be removed',
146
+ })[reason] || reason;
147
+
148
+ const defaultRemove = async targetPath => fsPromises.rm(targetPath, { recursive: true, force: true });
149
+
150
+ /**
151
+ * Delete every orphaned store. Stops early once `stopWhenFreeMB` megabytes are
152
+ * free, which is what lets the disk guard reclaim only as much as a task needs.
153
+ *
154
+ * @returns {Promise<{removed: Array<string>, skipped: Array<object>, freeMB: number|null}>}
155
+ */
156
+ export const reclaimAgentSnapshotStores = async ({ dataHome = getAgentDataHome(), minIdleMs = DEFAULT_AGENT_SNAPSHOT_MIN_IDLE_MS, stopWhenFreeMB = null, getFreeMB = null, now = Date.now, log = async () => {}, fileSystem = fsPromises, remove = defaultRemove } = {}) => {
157
+ const { orphaned, keep } = await classifyAgentSnapshotStores({ dataHome, minIdleMs, now, fileSystem });
158
+ const removed = [];
159
+ const skipped = keep.map(store => ({ path: store.path, reason: store.reason }));
160
+ let freeMB = getFreeMB ? await getFreeMB(dataHome) : null;
161
+ for (const store of orphaned) {
162
+ if (stopWhenFreeMB !== null && freeMB !== null && freeMB >= stopWhenFreeMB) break;
163
+ try {
164
+ await remove(store.path);
165
+ removed.push(store.path);
166
+ await log(` 🧹 Reclaimed orphaned agent snapshot store: ${store.path} (${describeAgentSnapshotReason(store.reason)})`);
167
+ } catch (error) {
168
+ skipped.push({ path: store.path, reason: 'remove_failed', error });
169
+ await log(` ⚠️ Could not remove ${store.path}: ${error.message}`, { level: 'warning' });
170
+ continue;
171
+ }
172
+ if (getFreeMB) freeMB = await getFreeMB(dataHome);
173
+ }
174
+ return { removed, skipped, freeMB };
175
+ };
176
+
177
+ /**
178
+ * Upper bound on directory entries visited while sizing the agent data home.
179
+ * The whole point of issue #2186 is that this tree can hold tens of gigabytes of
180
+ * loose git objects, so an unbounded `du` in the middle of a logging call would
181
+ * be the second bug. When the cap is hit the result is reported as `truncated`
182
+ * and the byte count is a lower bound.
183
+ */
184
+ export const AGENT_SNAPSHOT_USAGE_ENTRY_LIMIT = 20_000;
185
+
186
+ /**
187
+ * How much disk the agent snapshot stores currently occupy, for the resource
188
+ * snapshots in `solve.resource-diagnostics.lib.mjs`. Before issue #2186 the
189
+ * `RESOURCE_PHASE_AFTER_AGENT` line only showed a flat `/` reading, so 5 GB/h of
190
+ * growth under `~/.local/share` was invisible in the solve log.
191
+ *
192
+ * @returns {Promise<{path: string, count: number, bytes: number, truncated: boolean}>}
193
+ */
194
+ export const measureAgentSnapshotUsage = async ({ dataHome = getAgentDataHome(), fileSystem = fsPromises, entryLimit = AGENT_SNAPSHOT_USAGE_ENTRY_LIMIT } = {}) => {
195
+ const stores = await listAgentSnapshotStores({ dataHome, fileSystem });
196
+ const statPath = fileSystem.lstat ? fileSystem.lstat.bind(fileSystem) : fileSystem.stat.bind(fileSystem);
197
+ let bytes = 0;
198
+ let visited = 0;
199
+ let truncated = false;
200
+
201
+ const walk = async directory => {
202
+ let names;
203
+ try {
204
+ names = await fileSystem.readdir(directory);
205
+ } catch {
206
+ return;
207
+ }
208
+ for (const entry of names) {
209
+ if (visited >= entryLimit) {
210
+ truncated = true;
211
+ return;
212
+ }
213
+ const name = typeof entry === 'string' ? entry : entry?.name;
214
+ if (!name) continue;
215
+ visited += 1;
216
+ const child = path.join(directory, name);
217
+ let stats;
218
+ try {
219
+ stats = await statPath(child);
220
+ } catch {
221
+ continue;
222
+ }
223
+ if (stats.isDirectory()) {
224
+ await walk(child);
225
+ continue;
226
+ }
227
+ // Symlinks are counted as their own (tiny) size, never followed: agent
228
+ // 0.26.1 links stores to the repository object database, and following
229
+ // those would report the checkout's objects as agent state.
230
+ bytes += Number(stats.size) || 0;
231
+ }
232
+ };
233
+
234
+ for (const store of stores) {
235
+ await walk(store.path);
236
+ }
237
+
238
+ return { path: dataHome, count: stores.length, bytes, truncated };
239
+ };
240
+
241
+ export default {
242
+ AGENT_DATA_DIR_NAME,
243
+ AGENT_SNAPSHOT_USAGE_ENTRY_LIMIT,
244
+ classifyAgentSnapshotStores,
245
+ DEFAULT_AGENT_SNAPSHOT_MIN_IDLE_MS,
246
+ describeAgentSnapshotReason,
247
+ getAgentDataHome,
248
+ listAgentSnapshotStores,
249
+ measureAgentSnapshotUsage,
250
+ readAgentProjectWorktree,
251
+ reclaimAgentSnapshotStores,
252
+ };