@link-assistant/hive-mind 2.16.0 → 2.18.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.
- package/CHANGELOG.md +125 -0
- package/README.hi.md +12 -0
- package/README.md +15 -0
- package/README.ru.md +15 -0
- package/README.zh.md +24 -12
- package/package.json +24 -17
- package/src/agent-snapshot-store.lib.mjs +252 -0
- package/src/agent.lib.mjs +25 -25
- package/src/agent.version-gates.lib.mjs +73 -0
- package/src/bot-lifecycle.lib.mjs +55 -4
- package/src/cleanup.mjs +57 -3
- package/src/disk-guard.lib.mjs +21 -1
- package/src/formal-ai-version.lib.mjs +10 -6
- package/src/github-url-parser.lib.mjs +80 -23
- package/src/github-url-recovery.lib.mjs +514 -0
- package/src/hive.mjs +10 -0
- package/src/instrument.mjs +12 -14
- package/src/instrument.sanitize.lib.mjs +52 -0
- package/src/isolation-runner.lib.mjs +56 -33
- package/src/isolation-runner.parsers.lib.mjs +29 -3
- package/src/isolation-runner.resume.lib.mjs +263 -0
- package/src/locales/en.lino +7 -0
- package/src/locales/hi.lino +7 -0
- package/src/locales/ru.lino +7 -0
- package/src/locales/zh.lino +7 -0
- package/src/pull-request-changes.lib.mjs +1 -1
- package/src/session-kill-diagnostics.lib.mjs +47 -5
- package/src/session-kill-resume.in-place.lib.mjs +136 -0
- package/src/session-kill-resume.lib.mjs +43 -16
- package/src/session-monitor.kill-sections.lib.mjs +8 -0
- package/src/session-store.lib.mjs +1 -1
- package/src/solve.clone-errors.lib.mjs +86 -0
- package/src/solve.repository.lib.mjs +36 -63
- package/src/solve.resource-diagnostics.lib.mjs +34 -1
- package/src/solve.validation.lib.mjs +16 -0
- package/src/start-command-cli.lib.mjs +60 -0
- package/src/telegram-bot.mjs +51 -95
- package/src/telegram-overrides-validation.lib.mjs +73 -0
- 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.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3fc12ef: Recover broken GitHub URLs instead of rejecting them, and say what was repaired (issue #2194).
|
|
8
|
+
|
|
9
|
+
A user sent `/Claude https://github.com/G-Ivan-A/aether-orbis/pulls/30`. Telegram drew a healthy GitHub preview card under it, because github.com really does answer `/owner/repo/pulls/30` with **HTTP 200** and a full set of Open Graph tags — a genuinely wrong path like `/pullz/30` 404s and gets no card. The bot then refused the command and told the user to "1. Open the repository: https://github.com/G-Ivan-A/aether-orbis/**pulls/30**" — the same broken link that had just failed. Meanwhile `parseGitHubUrl` had already extracted the number: the pre-fix `case 'pulls'` stored `'30'` in `result.subpath` and nothing ever read it. The data to restore the URL from was sitting in the result object.
|
|
10
|
+
|
|
11
|
+
- **`src/github-url-recovery.lib.mjs`** repairs a URL before it is parsed: strips invisible characters (`\p{Cf}`/`\p{Cc}`/`\p{Zl}`/`\p{Zp}`, U+034F, variation selectors), unwraps `[title](url)` / `<url>` / `(url).`, folds full-width and fraction punctuation and full-width digits to ASCII, lower-cases scheme and host, reads `git@github.com:owner/repo.git` and `api.github.com/repos/...` as their web addresses, and reads `/pulls/30` as `/pull/30`, `/issue/123` as `/issues/123`, `/pull/30/files` as the pull request itself.
|
|
12
|
+
- **Recovery is on by default inside `parseGitHubUrl`**, so all 48 call sites across 14 files get it without changing any of them. `{ recover: false }` reproduces the old code path exactly, which is what makes the before/after evidence reproducible.
|
|
13
|
+
- **It never invents a GitHub URL.** `gitlab.com`, `bitbucket.org`, `gist.github.com`, `raw.githubusercontent.com`, `github.com.evil.example` and `evil.example/github.com/...` are all still rejected — and so is over-reach in the other direction: `support@github.com` is an email address rather than a repository (`git@github.com:owner/repo.git` still works), and `[the PR](aether-orbis)` stays rejected because unwrapping prose is only worth doing when what comes out already names github.com.
|
|
14
|
+
- **It never repairs silently.** Every result carries `original`, `repairs[]` and `recovered`, plus `hidden`/`revealed` when something invisible was removed. The Telegram bot (new `telegram.url_recovered` string in `en`/`ru`/`hi`/`zh`), `solve` and `hive` each print what they understood before acting on it.
|
|
15
|
+
- **Two silent-corruption bugs went with it.** A zero-width space in a repository name used to produce `valid: true` for `aether-orbis%E2%80%8B` — a repository that does not exist. `HTTPS://GITHUB.COM/...` used to parse as a _relative path_ with `HTTPS:` as the owner; the bot's own `url.includes('github.com')` gate had the same flaw and rejected the URL before the parser saw it — that gate now asks the recovery layer whether the text names github.com as its _host_, so `evil.example/github.com/...` no longer passes it either.
|
|
16
|
+
- **The log could not have proved an invisible character even if there had been one** — it never recorded the message text. `/solve` and its aliases now log raw text through `revealHiddenCharacters()` (a zero-width space appears as `[U+200B]`), and `traceUrlRecovery()` reports each repair stage under `--verbose`.
|
|
17
|
+
|
|
18
|
+
`normalize-url@9` and `confusables@1` were installed and measured against these exact inputs rather than assumed: `normalize-url` percent-encodes the zero-width space instead of removing it and **throws** on a full-width colon, and `confusables` turns `github.com/Ćwikła/...` into `github.com/Cwikla/...` — it would silently retarget a real owner. Neither knows that `/pulls/30` means pull request 30.
|
|
19
|
+
|
|
20
|
+
77 assertions in `tests/test-issue-2194-broken-url-recovery.mjs`. Timeline, evidence and the full analysis are in `docs/case-studies/issue-2194/README.md`, with reproduction scripts in `experiments/issue-2194/` and `examples/github-url-recovery-demo.mjs`.
|
|
21
|
+
|
|
22
|
+
## 2.17.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- 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).
|
|
27
|
+
|
|
28
|
+
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 `/`.
|
|
29
|
+
|
|
30
|
+
- **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.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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.
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
Evidence, reproduction scripts and the full analysis are in `docs/case-studies/issue-2186/README.md` and `experiments/issue-2186/`.
|
|
38
|
+
|
|
39
|
+
- 5e918d5: Fix the release failure and the CI/CD gaps behind issue #2198.
|
|
40
|
+
|
|
41
|
+
- `changeset version` no longer resolves `bun` from a stale root `bun.lock`:
|
|
42
|
+
the lockfile is removed and `devEngines.packageManager` declares npm, so
|
|
43
|
+
`@changesets/format` cannot spawn a package manager the runner does not have.
|
|
44
|
+
- New `scripts/check-package-manager.mjs` guard, run in the lint job, fails
|
|
45
|
+
loudly if a foreign lockfile or a missing package manager declaration comes
|
|
46
|
+
back.
|
|
47
|
+
- Lockfile changes now count as package changes in `detect-code-changes.mjs`,
|
|
48
|
+
so the guard cannot be gated out.
|
|
49
|
+
- Every workflow now declares the narrowest default `permissions` that still
|
|
50
|
+
allows checkout, instead of `read-all`, clearing zizmor's
|
|
51
|
+
`excessive-permissions` findings.
|
|
52
|
+
- secretlint was installed on every build but never run as a linter and had no
|
|
53
|
+
config; `npm run check:secrets` now runs it in the lint job, fail-closed,
|
|
54
|
+
with a narrow `.secretlintignore` for the fixtures that hold fake secrets on
|
|
55
|
+
purpose.
|
|
56
|
+
- A Broken Link Checker workflow (lychee, with a Wayback Machine fallback) and
|
|
57
|
+
an offline relative-link test now cover documentation links. They catch the
|
|
58
|
+
`docs/FREE_MODELS.md` case-study link that had pointed at a file which never
|
|
59
|
+
existed in any commit, in all four translations.
|
|
60
|
+
- All eight Docker jobs booted buildx through a bare
|
|
61
|
+
`docker/setup-buildx-action`, so a transient registry outage failed the
|
|
62
|
+
publish; they now go through a `setup-buildx-resilient` composite action that
|
|
63
|
+
pre-pulls the pinned BuildKit image with backoff and falls back to a
|
|
64
|
+
pull-through mirror.
|
|
65
|
+
- Nothing audited the dependency tree: CodeQL analyses our own source, and
|
|
66
|
+
`dependency-review-action` only runs on pull requests and only inspects the
|
|
67
|
+
dependencies a PR changes, so an advisory against a long-pinned package was
|
|
68
|
+
invisible to both. `security.yml` gains an `npm-audit` job running
|
|
69
|
+
`npm audit --package-lock-only --audit-level=high`, which also runs on the
|
|
70
|
+
existing schedule.
|
|
71
|
+
- actionlint is pinned to 1.7.12 instead of 1.7.7.
|
|
72
|
+
|
|
73
|
+
- Eleven links pointed at this repository under its former owner
|
|
74
|
+
(`deep-assistant/hive-mind`). The GitHub API reports that name as
|
|
75
|
+
`301 Moved Permanently`, but the pages a reader actually clicks answer 404,
|
|
76
|
+
so every one of them was broken. All rewritten, including six in a directory
|
|
77
|
+
the link checker excludes and would never have reported. The suppression list
|
|
78
|
+
it needed alongside them was also wrong: it carried a `www.npmjs.com` pattern
|
|
79
|
+
while the README links the bare host, so the rule matched nothing. Both are
|
|
80
|
+
now asserted — the guard checks that each suppressed URL is genuinely
|
|
81
|
+
_matched_ by a pattern, not merely mentioned near one.
|
|
82
|
+
- Kept the 1350-line warning threshold met after merging `main`: issue #2186 grew
|
|
83
|
+
`src/agent.lib.mjs` to 1357 lines, so the Agent CLI version floors moved to
|
|
84
|
+
`src/agent.version-gates.lib.mjs` (re-exported, so no importer changed).
|
|
85
|
+
- The change detector no longer trusts a PR head that was never tested. Issue
|
|
86
|
+
#1665's incremental `before..after` diff is correct only if the previous head
|
|
87
|
+
passed, and `cancel-in-progress` means it often had not: a docs commit pushed
|
|
88
|
+
minutes after a code commit cancels that commit's run and then skips the code
|
|
89
|
+
jobs itself, leaving the branch green over code no job ever ran.
|
|
90
|
+
`detect-code-changes.mjs` now asks the Actions API whether a successful run is
|
|
91
|
+
recorded for the previous head, and widens to the full PR diff whenever the
|
|
92
|
+
answer is anything else — including when the lookup itself fails, since an
|
|
93
|
+
unanswerable lookup must not be read as a "yes".
|
|
94
|
+
- F16: the F4 test matched npm's `allow-scripts` log prefix verbatim, so npm 11.19
|
|
95
|
+
renaming it to `install-scripts` turned `test-suites` red under a message claiming
|
|
96
|
+
npm had fixed the underlying bug. It had not — `linkPkg()` still resolves no
|
|
97
|
+
allowScripts policy, and `--ignore-scripts` is still the only lever that works.
|
|
98
|
+
The check now keys on the sentence that states the defect, pins both shipped
|
|
99
|
+
labels as samples, and no longer asserts a cause it cannot distinguish.
|
|
100
|
+
- F17: CodeQL flagged two of this PR's own test assertions as incomplete URL
|
|
101
|
+
sanitization. The alert was wrong about the security property — the flagged value
|
|
102
|
+
is a log the test's own mock wrote, not a URL anyone trusts — and right about the
|
|
103
|
+
code: `calls.includes('mirror.gcr.io')` was never what "the mirror was contacted"
|
|
104
|
+
meant. The assertions now compare registry hosts by equality, which retires both
|
|
105
|
+
alerts at the source and, being exact, immediately caught a retry budget no test
|
|
106
|
+
had been asserting.
|
|
107
|
+
|
|
108
|
+
- 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).
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
- **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.
|
|
113
|
+
- **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".
|
|
114
|
+
- **`$` 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.
|
|
115
|
+
- **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.
|
|
116
|
+
- **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.
|
|
117
|
+
|
|
118
|
+
Three of those bumps were behaviour changes that needed work rather than a version number:
|
|
119
|
+
|
|
120
|
+
- `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.
|
|
121
|
+
- `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.
|
|
122
|
+
- `@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.
|
|
123
|
+
|
|
124
|
+
`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.
|
|
125
|
+
|
|
126
|
+
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/`.
|
|
127
|
+
|
|
3
128
|
## 2.16.0
|
|
4
129
|
|
|
5
130
|
### Minor 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
|
|
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
|
+
| **节省时间** | 人类需要 2~8 小时的工作,AI 每个工作会话仅需 10~25 分钟完成。可批量执行仓库中的任务。"代码在你睡觉时已写好。" |
|
|
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.
|
|
3
|
+
"version": "2.18.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": "^
|
|
67
|
+
"@changesets/cli": "^3.0.2",
|
|
62
68
|
"@eslint/js": "^10.0.1",
|
|
63
|
-
"eslint": "^10.
|
|
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": "^
|
|
68
|
-
"lint-staged": "^17.
|
|
69
|
-
"prettier": "^3.
|
|
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.
|
|
74
|
-
"@secretlint/secretlint-rule-preset-recommend": "^13.0.
|
|
75
|
-
"@sentry/node": "^10.
|
|
76
|
-
"@sentry/profiling-node": "^10.
|
|
77
|
-
"agent-commander": "^0.
|
|
78
|
-
"dayjs": "^1.11.
|
|
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.
|
|
82
|
-
"lino-objects-codec": "^0.
|
|
83
|
-
"secretlint": "^13.0.
|
|
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.
|
|
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
|
+
};
|