@link-assistant/hive-mind 2.18.0 → 2.19.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 +32 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/package.json +3 -2
- package/src/agentic-cli-freshness.lib.mjs +118 -0
- package/src/agentic-cli-updater.lib.mjs +8 -4
- package/src/docker-sidecar.lib.mjs +17 -1
- package/src/formal-ai-image.lib.mjs +108 -10
- package/src/formal-ai-isolation.lib.mjs +15 -1
- package/src/formal-ai-runtime.lib.mjs +219 -6
- package/src/formal-ai-sidecar.lib.mjs +41 -9
- package/src/formal-ai.lib.mjs +5 -0
- package/src/hive-models.lib.mjs +181 -0
- package/src/hive-models.mjs +20 -0
- package/src/isolation-runner.lib.mjs +2 -2
- package/src/locales/en.lino +2 -1
- package/src/locales/hi.lino +2 -1
- package/src/locales/ru.lino +2 -1
- package/src/locales/zh.lino +2 -1
- package/src/model-catalogue-fetch.lib.mjs +333 -0
- package/src/model-catalogue-render.lib.mjs +191 -0
- package/src/model-catalogue-sources.lib.mjs +224 -0
- package/src/model-catalogue.lib.mjs +385 -0
- package/src/models/catalog.mjs +408 -0
- package/src/models/index.mjs +23 -362
- package/src/router-isolation.lib.mjs +103 -19
- package/src/router-routes.lib.mjs +250 -0
- package/src/router-sidecar.lib.mjs +33 -13
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.escalate.lib.mjs +3 -0
- package/src/solve.mjs +12 -0
- package/src/task.config.lib.mjs +5 -0
- package/src/task.mjs +12 -0
- package/src/telegram-bot.mjs +4 -1
- package/src/telegram-models-command.lib.mjs +157 -0
- package/src/telegram-ui-messages.lib.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.19.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6cdf9e2: Keep the Formal AI release a verified update accepted, and record the backend that actually served each task rather than the local wrapper (issues #2207, #2208, delivered together for #2209).
|
|
8
|
+
|
|
9
|
+
Two defects sat on either side of the same handoff, and each one hid the other. The updater verified a new Formal AI image against the persisted memory volume, accepted it, and wrote it into `formal-ai-sidecar.json` — and then the next cold task threw it away, because `acquireFormalAiSidecar` resolved its image from the environment alone and re-selected the bootstrap release. Meanwhile the task's provenance reported `formal-ai --version` from the local wrapper, a binary that answers no requests, so the log could not have revealed the downgrade even when it happened.
|
|
10
|
+
|
|
11
|
+
- **The accepted image is what the next task boots.** `readAcceptedFormalAiImage` reads the acceptance back out of state and acquire passes it into image resolution. Precedence is an operator pin (exact and exclusive), then the accepted image (also exclusive), then the published bootstrap image only when nothing has been accepted yet, then the local Hive Mind image. The accepted candidates lead with the **immutable digest** and fall back to the tag only with `expectDigest` attached, so a tag that moved after acceptance is refused instead of booted — which is not hypothetical: `ghcr.io/link-assistant/formal-ai:latest` pointed at 0.346.0 when this was reported and points at 0.347.0 now. An accepted image that is no longer on the host is a refusal with the pin named in the message, never a silent downgrade onto memory that has already been migrated.
|
|
12
|
+
- **The serving backend is asked, not assumed.** `probeFormalAiBackend` queries `/health` on the configured endpoint before any client runs, within a bounded deadline, presenting the configured API key and returning immediately on 401/403 instead of retrying a credential that will still be wrong. `assertSupportedFormalAiBackend` gives each answer its own refusal — unreachable, unauthorized, non-JSON, JSON without a version, below `FORMAL_AI_MINIMUM_VERSION`, incompatible persisted memory — and never guesses a version.
|
|
13
|
+
- **The two versions have two names.** `runtime.formalAiVersion` is the release that answers the requests and is what the logs and provenance report; `runtime.formalAiWrapperVersion` is the local executable that started the server and wrote the config, keeping its own floor check and its own log line. `validateFormalAiToolConnection` is unchanged.
|
|
14
|
+
- **The halves are joined.** When Hive Mind manages the sidecar, the leased image, digest, source and verified version are published into the task environment, read back by the runtime, and required to agree with what `/health` reports; an endpoint serving something other than the accepted release is refused rather than recorded. The runtime cache is keyed by endpoint, not by the release behind it, so a cache hit re-probes and says so when the backend changed underneath a reused URL.
|
|
15
|
+
|
|
16
|
+
Three regression suites cover it: `tests/test-issue-2207-accepted-image-persistence.mjs` (candidate selection, update → stopped sidecar → acquire → release → acquire including a process restart against persisted state, a moved tag, a rollback, a pruned accepted image, an operator pin with a busy lease), `tests/test-issue-2208-serving-backend-provenance.mjs` (ten cases against real loopback HTTP backends), and `tests/test-issue-2209-verified-release-serving-provenance.mjs`, which runs the Docker simulator and a real serving process together so neither half can look correct while the pair fails. The same transition was then run against a real Docker daemon and the real published images; the evidence is in `docs/case-studies/issue-2209/`.
|
|
17
|
+
|
|
18
|
+
## 2.19.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- ac07514: Stop the model list from ageing with the release: add the models that shipped since 2.17.0, hot-load the live catalogue from every source that costs nothing to read, expose it as `/models` and `hive-models`, teach `--use-router` both of the router's route dialects, and check the agentic CLI for a newer version before a task starts (issue #2202).
|
|
23
|
+
|
|
24
|
+
A provider can ship a model on a Tuesday, and until now Hive Mind could not name it until the next release: `src/models/index.mjs` was a static table and `--model` validated against nothing else. Six changes close that gap, none of which can spend a token or fail a run.
|
|
25
|
+
|
|
26
|
+
- **The new models are in the bundled table.** Fable 5.1, Mythos 5.1, GPT-6 Astra and GPT-5.6 Cyber, with their aliases, context windows and fallback chains. The additions pushed `src/models/index.mjs` past the 1350-line early warning of `scripts/check-file-line-limits.sh`, so the catalogue itself moved to `src/models/catalog.mjs` and is re-exported — the public surface is unchanged.
|
|
27
|
+
- **The catalogue hot-loads.** `src/model-catalogue.lib.mjs` merges six sources in a fixed precedence: the Link.Assistant Router's live catalogue, `codex debug models`, Anthropic's and OpenAI's `GET /v1/models`, models.dev for specifications, and the bundled table as the offline floor. models.dev only annotates; it never adds a model to the available list. Answers are cached per source and per tool for at least an hour, and `HIVE_MIND_MODEL_CATALOGUE_TTL_MINUTES` can only raise that floor.
|
|
28
|
+
- **Listing models cannot cost a token, structurally.** `assertTokenFreeSource` rejects any source that has not explicitly declared `billable: false`, so a source cannot become billable by omission, and `assertTokenFreeUrl` rejects any URL whose path is a completion endpoint no matter which descriptor carried it — a typo turning `/v1/models` into `/v1/messages` throws instead of spending. The four extraction methods that were considered and rejected, including driving either CLI's `/model` picker through its TUI, are recorded with their reasons so a future contributor does not re-derive them.
|
|
29
|
+
- **`/models` and `hive-models` show the whole spectrum.** Models are grouped as bundled-and-live, hot-loaded (a live source has it, this installation does not ship it), and bundled-only (shipped, unconfirmed), with `--tool`, `--details`, `--refresh`, `--json`, and `--all`. A footer names every source actually consulted, its outcome, and the age of the cache, so "why don't I see X" is answerable from the output — and a source that failed is described from its stderr, never by quoting the command line, so the router's leased token cannot ride a failure message into the footer. In Telegram the same flags are accepted the way a chat writes them: `/models codex`, `/models --tool codex` and `/models --tool=codex` are one command.
|
|
30
|
+
- **`--use-router` speaks both route dialects.** Router 1.0 removed every root, `/v1/*` and overlapping `/api/*` alias and moved each service under `/api/services/<service>/…` — measured, not assumed: on `1.2.0` every path that answers on `0.119.0` is a 404 and vice versa. `src/router-routes.lib.mjs` derives every base URL, the health probe and the catalogue path from the pinned image, so the pin can move in one line. It stays on `0.x` today, at `0.125.4`, because router `1.x` has no `gh`-reachable REST base and `gh` exposes no path-prefix option; the trade is reported by `describeRouterCoverageGaps` rather than discovered at runtime, and the upstream issues are filed.
|
|
31
|
+
- **A stale CLI is refreshed before the run needs it.** `/solve`, `/hive`, `/task`, `/fix`, `/models` and `hive-models` check the agentic CLI they are about to drive, because an outdated binary is the usual reason a brand-new model name is rejected. The check is throttled to once every six hours, narrowed to the one CLI in play, deferred entirely while other tasks are running, skipped for `--dry-run` and `--only-prepare-command`, and never fatal. Each run excludes its own task from the idle gate — otherwise a `/solve` asking for an update would find itself in the process table and defer forever. Opt out with `--no-tool-update` or `HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE=0`.
|
|
32
|
+
|
|
33
|
+
Documentation is in `docs/MODELS.md` and its `zh`/`hi`/`ru` siblings; the full analysis — the requirement-by-requirement plan, the router route measurement, and what shipped differently from the plan — is in `docs/case-studies/issue-2202/`.
|
|
34
|
+
|
|
3
35
|
## 2.18.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/README.hi.md
CHANGED
|
@@ -515,6 +515,8 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
515
515
|
|
|
516
516
|
> **📖 मुफ्त मॉडल गाइड**: OpenCode Zen और Kilo Gateway प्रदाताओं सहित सभी मुफ्त मॉडलों के बारे में व्यापक जानकारी के लिए [docs/FREE_MODELS.hi.md](./docs/FREE_MODELS.hi.md) देखें।
|
|
517
517
|
|
|
518
|
+
> **📖 लाइव मॉडल सूची**: यह देखने के लिए कि इस समय कौन-से मॉडल उपलब्ध हैं — उनमें वे भी जो इस installation के प्रकाशित होने के बाद जारी हुए — `hive-models` चलाएँ (या Telegram में `/models`)। देखें [docs/MODELS.hi.md](./docs/MODELS.hi.md)।
|
|
519
|
+
|
|
518
520
|
#### `/hive` - Hive ऑर्केस्ट्रेशन चलाएँ
|
|
519
521
|
|
|
520
522
|
```
|
package/README.md
CHANGED
|
@@ -533,6 +533,8 @@ See [docs/CONFIGURATION.md](./docs/CONFIGURATION.md) for the full per-tool defau
|
|
|
533
533
|
|
|
534
534
|
> **📖 Free Models Guide**: See [docs/FREE_MODELS.md](./docs/FREE_MODELS.md) for comprehensive information about all free models including OpenCode Zen and Kilo Gateway providers.
|
|
535
535
|
|
|
536
|
+
> **📖 Live model list**: run `hive-models` (or `/models` in Telegram) to see which models are reachable right now, including ones released after this installation was published. See [docs/MODELS.md](./docs/MODELS.md).
|
|
537
|
+
|
|
536
538
|
#### `/hive` - Run Hive Orchestration
|
|
537
539
|
|
|
538
540
|
```
|
package/README.ru.md
CHANGED
|
@@ -516,6 +516,8 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
516
516
|
|
|
517
517
|
> **📖 Руководство по бесплатным моделям**: см. [docs/FREE_MODELS.ru.md](./docs/FREE_MODELS.ru.md) для получения полной информации обо всех бесплатных моделях, включая провайдеры OpenCode Zen и Kilo Gateway.
|
|
518
518
|
|
|
519
|
+
> **📖 Живой список моделей**: запустите `hive-models` (или `/models` в Telegram), чтобы увидеть, какие модели доступны прямо сейчас, включая вышедшие уже после публикации этой установки. См. [docs/MODELS.ru.md](./docs/MODELS.ru.md).
|
|
520
|
+
|
|
519
521
|
#### `/hive` — Запуск оркестрации Hive
|
|
520
522
|
|
|
521
523
|
```
|
package/README.zh.md
CHANGED
|
@@ -512,6 +512,8 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
512
512
|
|
|
513
513
|
> **📖 免费模型指南**:有关所有免费模型(包括 OpenCode Zen 和 Kilo Gateway 提供商)的全面信息,请参见 [docs/FREE_MODELS.zh.md](./docs/FREE_MODELS.zh.md)。
|
|
514
514
|
|
|
515
|
+
> **📖 实时模型列表**:运行 `hive-models`(或在 Telegram 中使用 `/models`)即可查看当前可用的模型,包括本次安装发布之后才推出的模型。参见 [docs/MODELS.zh.md](./docs/MODELS.zh.md)。
|
|
516
|
+
|
|
515
517
|
#### `/hive` - 运行蜂群编排
|
|
516
518
|
|
|
517
519
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.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",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"configure-claude": "./src/configure-claude.mjs",
|
|
15
15
|
"start-screen": "./src/start-screen.mjs",
|
|
16
16
|
"hive-screens": "./src/hive-screens.mjs",
|
|
17
|
+
"hive-models": "./src/hive-models.mjs",
|
|
17
18
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"changeset": "changeset",
|
|
31
32
|
"changeset:version": "changeset version",
|
|
32
33
|
"changeset:publish": "npm run build:pre && changeset publish",
|
|
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",
|
|
34
|
+
"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/hive-models.mjs && chmod +x src/telegram-bot.mjs",
|
|
34
35
|
"prepare": "husky"
|
|
35
36
|
},
|
|
36
37
|
"repository": {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight CLI freshness check (issue #2202, R6).
|
|
3
|
+
*
|
|
4
|
+
* R6: "/models and each /solve and other commands that relevant for
|
|
5
|
+
* claude/codex tools should check if new version available, and before
|
|
6
|
+
* starting task execution or before providing new models list - we should
|
|
7
|
+
* update them."
|
|
8
|
+
*
|
|
9
|
+
* `updateAgenticClisWhenIdle` already knows how to do the refresh safely — it
|
|
10
|
+
* throttles registry reads, takes a state lock, and refuses to swap a binary
|
|
11
|
+
* out from under a running task. What it lacked was a caller other than the
|
|
12
|
+
* Telegram maintenance tick, and two things a command entry point needs:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Narrowing.** `hive-models --tool codex` should not reinstall Gemini.
|
|
15
|
+
* 2. **Not counting itself as busy.** The idle gate scans `/proc` for running
|
|
16
|
+
* solve/task processes by issue reference. A solve run that checks for
|
|
17
|
+
* updates after it has started would find *itself* and defer forever, so
|
|
18
|
+
* the caller passes its own task reference to be ignored.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is best-effort: a refresh failure must never stop the command
|
|
21
|
+
* the operator actually asked for.
|
|
22
|
+
*
|
|
23
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2202
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { AGENTIC_CLI_TARGETS, isAgenticCliAutoUpdateEnabled, updateAgenticClisWhenIdle } from './agentic-cli-updater.lib.mjs';
|
|
27
|
+
|
|
28
|
+
const KNOWN_TOOL_IDS = new Set(AGENTIC_CLI_TARGETS.map(target => target.id));
|
|
29
|
+
|
|
30
|
+
/** Tool aliases Hive Mind commands use that are not the updater's target id. */
|
|
31
|
+
export const FRESHNESS_TOOL_ALIASES = Object.freeze({
|
|
32
|
+
'claude-code': 'claude',
|
|
33
|
+
'gemini-cli': 'gemini',
|
|
34
|
+
'qwen-code': 'qwen',
|
|
35
|
+
'github-copilot': 'copilot',
|
|
36
|
+
'opencode-ai': 'opencode',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** Normalize whatever a command calls its tool into updater target ids. */
|
|
40
|
+
export const resolveFreshnessTools = tools => {
|
|
41
|
+
const requested = (Array.isArray(tools) ? tools : [tools])
|
|
42
|
+
.flatMap(entry =>
|
|
43
|
+
String(entry ?? '')
|
|
44
|
+
.split(',')
|
|
45
|
+
.map(part => part.trim().toLowerCase())
|
|
46
|
+
)
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
const resolved = [];
|
|
49
|
+
for (const entry of requested) {
|
|
50
|
+
const id = FRESHNESS_TOOL_ALIASES[entry] ?? entry;
|
|
51
|
+
if (KNOWN_TOOL_IDS.has(id) && !resolved.includes(id)) resolved.push(id);
|
|
52
|
+
}
|
|
53
|
+
return resolved;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** `https://github.com/o/r/issues/7` → `{owner:'o', repo:'r', number:7}`. */
|
|
57
|
+
export const parseTaskRef = value => {
|
|
58
|
+
if (!value) return null;
|
|
59
|
+
if (typeof value === 'object') {
|
|
60
|
+
const number = Number(value.number);
|
|
61
|
+
if (!value.owner || !value.repo || !Number.isFinite(number)) return null;
|
|
62
|
+
return { owner: String(value.owner), repo: String(value.repo), number };
|
|
63
|
+
}
|
|
64
|
+
const match = String(value).match(/github\.com\/([^/\s]+)\/([^/\s]+)\/(?:issues|pull)\/(\d+)/i);
|
|
65
|
+
if (!match) return null;
|
|
66
|
+
return { owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sameRef = (a, b) => a.owner.toLowerCase() === b.owner.toLowerCase() && a.repo.toLowerCase() === b.repo.toLowerCase() && a.number === b.number;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Refresh the agentic CLIs a command is about to drive.
|
|
73
|
+
*
|
|
74
|
+
* Never throws: every failure path returns a status the caller can log and
|
|
75
|
+
* ignore. Statuses come from `updateAgenticClisWhenIdle`
|
|
76
|
+
* (`checked`/`throttled`/`busy`/`disabled`) plus `skipped` for a caller opt-out
|
|
77
|
+
* and `error` when the refresh itself blew up.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} options
|
|
80
|
+
* @param {string|string[]} options.tools tool ids/aliases the command needs
|
|
81
|
+
* @param {boolean} options.enabled false for `--no-update`
|
|
82
|
+
* @param {Array} options.ignoreTasks task refs or GitHub URLs that are *this* run
|
|
83
|
+
*/
|
|
84
|
+
export const ensureAgenticCliFreshness = async ({ tools = [], env = process.env, log = null, verbose = false, force = false, enabled = true, ignoreTasks = [], getActiveTasksImpl = null, updateImpl = updateAgenticClisWhenIdle, minIntervalMs = undefined } = {}) => {
|
|
85
|
+
const only = resolveFreshnessTools(tools);
|
|
86
|
+
if (!enabled) return { status: 'skipped', reason: 'the caller disabled the update check (--no-update)', tools: only, updated: [], upToDate: [], failed: [] };
|
|
87
|
+
if (!isAgenticCliAutoUpdateEnabled(env)) return { status: 'disabled', reason: 'HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE is off', tools: only, updated: [], upToDate: [], failed: [] };
|
|
88
|
+
|
|
89
|
+
const ignored = ignoreTasks.map(parseTaskRef).filter(Boolean);
|
|
90
|
+
let activeTasks = getActiveTasksImpl;
|
|
91
|
+
if (ignored.length > 0) {
|
|
92
|
+
const inner = getActiveTasksImpl ?? (await import('./cleanup.os.lib.mjs')).getActiveTasks;
|
|
93
|
+
activeTasks = async options => {
|
|
94
|
+
const tasks = await inner(options);
|
|
95
|
+
return tasks.filter(task => !ignored.some(ref => sameRef(ref, { owner: task.owner, repo: task.repo, number: Number(task.number) })));
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await updateImpl({ env, log, verbose, force, only, ...(activeTasks ? { getActiveTasksImpl: activeTasks } : {}), ...(minIntervalMs === undefined ? {} : { minIntervalMs }) });
|
|
101
|
+
return { tools: only, updated: [], upToDate: [], failed: [], ...result };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const message = String(error?.message ?? error);
|
|
104
|
+
if (verbose && log) await log(`[VERBOSE] agentic-cli-freshness: refresh failed — ${message}`);
|
|
105
|
+
return { status: 'error', reason: message, tools: only, updated: [], upToDate: [], failed: [] };
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** One human-readable line summarising a freshness result, or null when there is nothing to say. */
|
|
110
|
+
export const describeFreshnessResult = result => {
|
|
111
|
+
if (!result) return null;
|
|
112
|
+
if (result.updated?.length > 0) return `⬆️ Updated ${result.updated.map(entry => `${entry.id} ${entry.from} → ${entry.to}`).join(', ')}`;
|
|
113
|
+
if (result.failed?.length > 0) return `⚠️ Could not update ${result.failed.map(entry => entry.id).join(', ')}`;
|
|
114
|
+
if (result.status === 'busy') return 'Skipped the CLI update check: other tasks are running.';
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default { FRESHNESS_TOOL_ALIASES, describeFreshnessResult, ensureAgenticCliFreshness, parseTaskRef, resolveFreshnessTools };
|
|
@@ -81,10 +81,14 @@ const parseIdList = value =>
|
|
|
81
81
|
* `HIVE_MIND_AGENTIC_CLI_UPDATE_ONLY` is an allow-list and
|
|
82
82
|
* `HIVE_MIND_AGENTIC_CLI_UPDATE_EXCLUDE` a deny-list, both by target id.
|
|
83
83
|
*/
|
|
84
|
-
export const listAgenticCliUpdateTargets = (env = process.env) => {
|
|
84
|
+
export const listAgenticCliUpdateTargets = (env = process.env, { only: requested = [] } = {}) => {
|
|
85
85
|
const only = parseIdList(env.HIVE_MIND_AGENTIC_CLI_UPDATE_ONLY);
|
|
86
86
|
const excluded = new Set(parseIdList(env.HIVE_MIND_AGENTIC_CLI_UPDATE_EXCLUDE));
|
|
87
|
-
|
|
87
|
+
// A caller-supplied narrowing (issue #2202, R6: refresh the CLIs a command is
|
|
88
|
+
// about to drive) intersects with the operator's allow-list rather than
|
|
89
|
+
// overriding it — an operator who excluded a CLI still gets it excluded.
|
|
90
|
+
const caller = parseIdList(Array.isArray(requested) ? requested.join(',') : requested);
|
|
91
|
+
return AGENTIC_CLI_TARGETS.filter(target => (only.length === 0 || only.includes(target.id)) && (caller.length === 0 || caller.includes(target.id)) && !excluded.has(target.id));
|
|
88
92
|
};
|
|
89
93
|
|
|
90
94
|
/** First semantic version in a CLI's `--version` output, which is rarely bare. */
|
|
@@ -147,7 +151,7 @@ export const installAgenticCli = async (target, { run = execFileAsync, timeoutMs
|
|
|
147
151
|
*
|
|
148
152
|
* @returns {Promise<{status: 'disabled'|'busy'|'throttled'|'checked', updated: object[], upToDate: object[], failed: object[]}>}
|
|
149
153
|
*/
|
|
150
|
-
export const updateAgenticClisWhenIdle = async ({ env = process.env, fsImpl = fs, run = execFileAsync, log = null, verbose = false, getActiveTasksImpl = null, now = () => new Date(), minIntervalMs = DEFAULT_CLI_UPDATE_INTERVAL_MS, force = false, lockOptions = {} } = {}) => {
|
|
154
|
+
export const updateAgenticClisWhenIdle = async ({ env = process.env, fsImpl = fs, run = execFileAsync, log = null, verbose = false, getActiveTasksImpl = null, now = () => new Date(), minIntervalMs = DEFAULT_CLI_UPDATE_INTERVAL_MS, force = false, only = [], lockOptions = {} } = {}) => {
|
|
151
155
|
if (!isAgenticCliAutoUpdateEnabled(env)) {
|
|
152
156
|
if (verbose && log) await log('[VERBOSE] agentic-cli-updater: disabled by HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE');
|
|
153
157
|
return { status: 'disabled', updated: [], upToDate: [], failed: [] };
|
|
@@ -176,7 +180,7 @@ export const updateAgenticClisWhenIdle = async ({ env = process.env, fsImpl = fs
|
|
|
176
180
|
const failed = [];
|
|
177
181
|
const tools = { ...state.tools };
|
|
178
182
|
|
|
179
|
-
for (const target of listAgenticCliUpdateTargets(env)) {
|
|
183
|
+
for (const target of listAgenticCliUpdateTargets(env, { only })) {
|
|
180
184
|
const installed = await readInstalledCliVersion(target, { run });
|
|
181
185
|
if (!installed) {
|
|
182
186
|
// Not installed on this host (image variants differ); nothing to refresh.
|
|
@@ -49,8 +49,24 @@ export const dockerOk = async (run, args, options) => {
|
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Mask the secrets a `docker` argv carries.
|
|
54
|
+
*
|
|
55
|
+
* `execFile` builds a failed command's message as "Command failed: <argv…>",
|
|
56
|
+
* and the argvs assembled here carry real secrets: the router's
|
|
57
|
+
* `--env TOKEN_SECRET=…`, a leased `--env ROUTER_CATALOGUE_TOKEN=…`, a
|
|
58
|
+
* provider's `--api-key …`. stderr is preferred over that message precisely
|
|
59
|
+
* because it describes the failure without the command line, but stderr is
|
|
60
|
+
* empty when the process is killed on a timeout — so whatever survives is
|
|
61
|
+
* masked before anyone reads it.
|
|
62
|
+
*/
|
|
63
|
+
const maskDockerArgvSecrets = text =>
|
|
64
|
+
String(text ?? '')
|
|
65
|
+
.replace(/(--env[= ])([A-Za-z_][A-Za-z0-9_]*)=(\S+)/g, '$1$2=***')
|
|
66
|
+
.replace(/(--(?:api-key|token|secret|password)[= ])(\S+)/g, '$1***');
|
|
67
|
+
|
|
52
68
|
/** The message a failed `docker` invocation should be reported with. */
|
|
53
|
-
export const dockerErrorMessage = error => error?.stderr?.toString?.().trim() || error?.message || String(error);
|
|
69
|
+
export const dockerErrorMessage = error => maskDockerArgvSecrets(error?.stderr?.toString?.().trim() || error?.message || String(error));
|
|
54
70
|
|
|
55
71
|
/**
|
|
56
72
|
* Inspect a container without treating "absent" as an error.
|
|
@@ -35,15 +35,20 @@
|
|
|
35
35
|
* - `HIVE_MIND_FORMAL_AI_IMAGE` is an **exact** operator pin: it is the only
|
|
36
36
|
* candidate, and if it cannot be resolved the acquire fails. An operator who
|
|
37
37
|
* names an image means that image.
|
|
38
|
-
* - Otherwise the
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* - Otherwise the image accepted by the last verified idle update wins, by its
|
|
39
|
+
* immutable digest (issue #2207). Before that rule the bootstrap version was
|
|
40
|
+
* re-selected on every cold start, so an accepted update was discarded by the
|
|
41
|
+
* next task and the sidecar returned to the release Hive Mind ships with.
|
|
42
|
+
* - Otherwise the published bootstrap image is preferred, and the local Hive
|
|
43
|
+
* Mind image is the fallback. The fallback is only *used*; it is never pulled,
|
|
44
|
+
* because its whole point is that it is already on the host.
|
|
41
45
|
* - Nothing here downgrades a Formal AI task to another model. Issue #2146's
|
|
42
46
|
* fail-closed rule still holds: when no candidate resolves, the task is
|
|
43
47
|
* refused with an actionable message.
|
|
44
48
|
*
|
|
45
49
|
* @see https://github.com/link-assistant/hive-mind/issues/2154
|
|
46
50
|
* @see https://github.com/link-assistant/hive-mind/issues/2146
|
|
51
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2207
|
|
47
52
|
*/
|
|
48
53
|
|
|
49
54
|
import { execFile } from 'node:child_process';
|
|
@@ -62,6 +67,7 @@ const DEFAULT_DOCKER_TIMEOUT_MS = 600_000;
|
|
|
62
67
|
/** Where each candidate came from, so logs and errors can explain themselves. */
|
|
63
68
|
export const FORMAL_AI_IMAGE_SOURCES = Object.freeze({
|
|
64
69
|
PINNED: 'operator-pin',
|
|
70
|
+
ACCEPTED: 'accepted-update',
|
|
65
71
|
PUBLISHED: 'published-image',
|
|
66
72
|
HIVE_MIND: 'hive-mind-image',
|
|
67
73
|
});
|
|
@@ -114,15 +120,82 @@ export const classifyDockerRegistryError = message => {
|
|
|
114
120
|
*/
|
|
115
121
|
export const resolveFormalAiFallbackImage = (env = process.env) => String(env.HIVE_MIND_FORMAL_AI_FALLBACK_IMAGE || '').trim() || getDockerIsolationImage({ env });
|
|
116
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The last image an idle update actually accepted, read out of the durable
|
|
125
|
+
* sidecar record (issue #2207).
|
|
126
|
+
*
|
|
127
|
+
* `state.image`/`state.imageDigest` are a *cache of what is running* — every
|
|
128
|
+
* acquire overwrites them with the container it found — so they cannot answer
|
|
129
|
+
* "which image did we verify?". `state.lastUpdate` can: it is written only by
|
|
130
|
+
* `updateFormalAiSidecarWhenIdle` after the pull, the side-effect-free memory
|
|
131
|
+
* preflight, the backed-up migration and the `/health` memory-compatibility
|
|
132
|
+
* check have all succeeded, and it is left untouched by a rollback.
|
|
133
|
+
*
|
|
134
|
+
* @param {object|null} state - A `formal-ai-sidecar.json` record.
|
|
135
|
+
* @returns {{image: string|null, digest: string|null, version: string|null, memorySchemaVersion: (number|string|null), updatedAt: string|null}|null}
|
|
136
|
+
*/
|
|
137
|
+
export const readAcceptedFormalAiImage = state => {
|
|
138
|
+
const update = state?.lastUpdate;
|
|
139
|
+
if (!update || typeof update !== 'object') return null;
|
|
140
|
+
const image = String(update.image || '').trim() || null;
|
|
141
|
+
const digest = String(update.digest || '').trim() || null;
|
|
142
|
+
if (!image && !digest) return null;
|
|
143
|
+
return { image, digest, version: update.version ?? null, memorySchemaVersion: update.memorySchemaVersion ?? null, updatedAt: update.updatedAt ?? null };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Candidates that reproduce an accepted update, most trustworthy first.
|
|
148
|
+
*
|
|
149
|
+
* Two rules make this safe:
|
|
150
|
+
*
|
|
151
|
+
* - **The digest wins.** `docker image inspect --format {{.Id}}` returns the
|
|
152
|
+
* local content address, which Docker also accepts as a run reference. Using
|
|
153
|
+
* it means a tag that moved after acceptance (`:latest` is the update tag)
|
|
154
|
+
* cannot smuggle an unverified image past the memory verification that
|
|
155
|
+
* earned the acceptance.
|
|
156
|
+
* - **The tag is only a recovery path.** If the accepted image ID has been
|
|
157
|
+
* pruned, the recorded reference may be pulled again, but the result is
|
|
158
|
+
* accepted only when its digest still matches; otherwise the candidate is
|
|
159
|
+
* refused rather than booted.
|
|
160
|
+
*
|
|
161
|
+
* @param {object|null} accepted - Output of {@link readAcceptedFormalAiImage}.
|
|
162
|
+
* @returns {Array<object>}
|
|
163
|
+
*/
|
|
164
|
+
export const resolveAcceptedFormalAiImageCandidates = (accepted = null) => {
|
|
165
|
+
if (!accepted) return [];
|
|
166
|
+
const candidates = [];
|
|
167
|
+
if (accepted.digest) candidates.push({ image: accepted.digest, reference: accepted.image || accepted.digest, source: FORMAL_AI_IMAGE_SOURCES.ACCEPTED, pullable: false, accepted: true });
|
|
168
|
+
if (accepted.image && accepted.digest) candidates.push({ image: accepted.image, reference: accepted.image, source: FORMAL_AI_IMAGE_SOURCES.ACCEPTED, pullable: true, accepted: true, expectDigest: accepted.digest });
|
|
169
|
+
// An acceptance recorded before digests were captured: the reference is all
|
|
170
|
+
// there is, so it may be used but never re-pulled behind a moving tag.
|
|
171
|
+
if (accepted.image && !accepted.digest) candidates.push({ image: accepted.image, reference: accepted.image, source: FORMAL_AI_IMAGE_SOURCES.ACCEPTED, pullable: false, accepted: true });
|
|
172
|
+
return candidates;
|
|
173
|
+
};
|
|
174
|
+
|
|
117
175
|
/**
|
|
118
176
|
* Ordered list of images to try, most preferred first.
|
|
119
177
|
*
|
|
178
|
+
* Precedence, and why:
|
|
179
|
+
*
|
|
180
|
+
* 1. `HIVE_MIND_FORMAL_AI_IMAGE` — an operator who names an image means that
|
|
181
|
+
* image; the pin is exact and exclusive.
|
|
182
|
+
* 2. The last accepted, verified update (issue #2207). Also exclusive: once an
|
|
183
|
+
* image has been verified against the migrated persisted memory, quietly
|
|
184
|
+
* booting the older bootstrap release against that memory is exactly the
|
|
185
|
+
* silent downgrade the fail-closed rule forbids.
|
|
186
|
+
* 3. The bootstrap release, then the local Hive Mind image — the cold-start
|
|
187
|
+
* path for a host that has never completed an update.
|
|
188
|
+
*
|
|
120
189
|
* @param {object} [env]
|
|
190
|
+
* @param {object} [options]
|
|
191
|
+
* @param {object|null} [options.accepted] - Output of {@link readAcceptedFormalAiImage}.
|
|
121
192
|
* @returns {Array<{image: string, source: string, pullable: boolean}>}
|
|
122
193
|
*/
|
|
123
|
-
export const resolveFormalAiSidecarImageCandidates = (env = process.env) => {
|
|
194
|
+
export const resolveFormalAiSidecarImageCandidates = (env = process.env, { accepted = null } = {}) => {
|
|
124
195
|
const pinned = String(env.HIVE_MIND_FORMAL_AI_IMAGE || '').trim();
|
|
125
196
|
if (pinned) return [{ image: pinned, source: FORMAL_AI_IMAGE_SOURCES.PINNED, pullable: true }];
|
|
197
|
+
const acceptedCandidates = resolveAcceptedFormalAiImageCandidates(accepted);
|
|
198
|
+
if (acceptedCandidates.length > 0) return acceptedCandidates;
|
|
126
199
|
const candidates = [{ image: `${FORMAL_AI_IMAGE_REPOSITORY}:${FORMAL_AI_BOOTSTRAP_VERSION}`, source: FORMAL_AI_IMAGE_SOURCES.PUBLISHED, pullable: true }];
|
|
127
200
|
const fallback = resolveFormalAiFallbackImage(env);
|
|
128
201
|
// Never pulled: the fallback's value is that it is already on the host. A
|
|
@@ -132,7 +205,7 @@ export const resolveFormalAiSidecarImageCandidates = (env = process.env) => {
|
|
|
132
205
|
};
|
|
133
206
|
|
|
134
207
|
/** The preferred image, kept for callers and logs that only need the headline reference. */
|
|
135
|
-
export const resolveFormalAiSidecarImage = (env = process.env) => resolveFormalAiSidecarImageCandidates(env)[0].image;
|
|
208
|
+
export const resolveFormalAiSidecarImage = (env = process.env, options = {}) => resolveFormalAiSidecarImageCandidates(env, options)[0].image;
|
|
136
209
|
|
|
137
210
|
const inspectLocalImage = async (image, { run, timeoutMs }) => {
|
|
138
211
|
try {
|
|
@@ -148,7 +221,8 @@ const inspectLocalImage = async (image, { run, timeoutMs }) => {
|
|
|
148
221
|
* bot log knows the cause and the fix without opening a shell.
|
|
149
222
|
*/
|
|
150
223
|
const describeFailure = attempts => {
|
|
151
|
-
const
|
|
224
|
+
const accepted = attempts.some(attempt => attempt.source === FORMAL_AI_IMAGE_SOURCES.ACCEPTED);
|
|
225
|
+
const lines = [accepted ? 'The Formal AI image accepted by the last verified update could not be resolved, so the sidecar was not started.' : 'No Formal AI image could be resolved, so the sidecar was not started.'];
|
|
152
226
|
for (const attempt of attempts) {
|
|
153
227
|
if (attempt.source === FORMAL_AI_IMAGE_SOURCES.HIVE_MIND && attempt.kind === 'absent') {
|
|
154
228
|
lines.push(`• ${attempt.image} (local Hive Mind image, fallback): not present on this host — Docker-isolated tasks would have to pull it too.`);
|
|
@@ -157,6 +231,13 @@ const describeFailure = attempts => {
|
|
|
157
231
|
lines.push(`• ${attempt.image} (${attempt.source}): ${attempt.reason}${attempt.error ? ` — ${attempt.error}` : ''}`);
|
|
158
232
|
for (const step of attempt.remediation ?? []) lines.push(` → ${step}`);
|
|
159
233
|
}
|
|
234
|
+
if (accepted) {
|
|
235
|
+
// Falling back to the bootstrap release here would run an *older* binary
|
|
236
|
+
// against memory that the accepted release already migrated (issue #2207).
|
|
237
|
+
lines.push('Hive Mind will not fall back to an older Formal AI release against memory an accepted update already migrated.');
|
|
238
|
+
lines.push(' → restore the accepted image on this host (`docker pull` it, or copy it back), or');
|
|
239
|
+
lines.push(' → set HIVE_MIND_FORMAL_AI_IMAGE to the image you want this host to run, which overrides the accepted update.');
|
|
240
|
+
}
|
|
160
241
|
lines.push('Formal AI tasks fail closed by design (issue #2146): Hive Mind will not silently run them on another model.');
|
|
161
242
|
return lines.join('\n');
|
|
162
243
|
};
|
|
@@ -172,17 +253,27 @@ const describeFailure = attempts => {
|
|
|
172
253
|
* @param {Function|null} [params.log]
|
|
173
254
|
* @param {boolean} [params.verbose]
|
|
174
255
|
* @param {boolean} [params.pull] - Set false to accept only images already on the host.
|
|
175
|
-
* @
|
|
256
|
+
* @param {object|null} [params.accepted] - Output of {@link readAcceptedFormalAiImage}, used to build the default candidates.
|
|
257
|
+
* @returns {Promise<{image: string, reference: string, source: string, digest: string|null, pulled: boolean, attempts: object[]}>}
|
|
176
258
|
* @throws {Error} When no candidate resolves; the message names every attempt and its fix.
|
|
177
259
|
*/
|
|
178
|
-
export const ensureFormalAiSidecarImage = async ({ env = process.env, run = execFileAsync, timeoutMs = DEFAULT_DOCKER_TIMEOUT_MS, log = null, verbose = false, pull = true, candidates = resolveFormalAiSidecarImageCandidates(env) } = {}) => {
|
|
260
|
+
export const ensureFormalAiSidecarImage = async ({ env = process.env, run = execFileAsync, timeoutMs = DEFAULT_DOCKER_TIMEOUT_MS, log = null, verbose = false, pull = true, accepted = null, candidates = resolveFormalAiSidecarImageCandidates(env, { accepted }) } = {}) => {
|
|
179
261
|
const attempts = [];
|
|
262
|
+
const reference = candidate => candidate.reference || candidate.image;
|
|
263
|
+
// A candidate that names an expected digest is a *recovery* path for an
|
|
264
|
+
// accepted update whose image ID was pruned: the reference may be re-fetched,
|
|
265
|
+
// but only the recorded content address is allowed to run (issue #2207).
|
|
266
|
+
const mismatch = (candidate, digest) => ({ ...candidate, kind: 'digest-mismatch', reason: `resolved to ${digest ?? 'no digest'} instead of the accepted ${candidate.expectDigest}`, error: null, remediation: ['the tag moved after the update was accepted; restore the accepted image or repin with HIVE_MIND_FORMAL_AI_IMAGE'] });
|
|
180
267
|
|
|
181
268
|
for (const candidate of candidates) {
|
|
182
269
|
const localDigest = await inspectLocalImage(candidate.image, { run, timeoutMs });
|
|
270
|
+
if (localDigest && candidate.expectDigest && localDigest !== candidate.expectDigest) {
|
|
271
|
+
attempts.push(mismatch(candidate, localDigest));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
183
274
|
if (localDigest) {
|
|
184
275
|
if (verbose && log) await log(`[VERBOSE] formal-ai-image: using '${candidate.image}' (${candidate.source}) already present locally, digest=${localDigest}`);
|
|
185
|
-
return { image: candidate.image, source: candidate.source, digest: localDigest, pulled: false, attempts };
|
|
276
|
+
return { image: candidate.image, reference: reference(candidate), source: candidate.source, digest: localDigest, pulled: false, attempts };
|
|
186
277
|
}
|
|
187
278
|
|
|
188
279
|
if (!candidate.pullable || !pull) {
|
|
@@ -202,8 +293,13 @@ export const ensureFormalAiSidecarImage = async ({ env = process.env, run = exec
|
|
|
202
293
|
}
|
|
203
294
|
|
|
204
295
|
const digest = await inspectLocalImage(candidate.image, { run, timeoutMs });
|
|
296
|
+
if (candidate.expectDigest && digest !== candidate.expectDigest) {
|
|
297
|
+
attempts.push(mismatch(candidate, digest));
|
|
298
|
+
if (log) await log(`⚠️ ${candidate.image} no longer resolves to the accepted digest ${candidate.expectDigest}; refusing to boot it.`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
205
301
|
if (verbose && log) await log(`[VERBOSE] formal-ai-image: pulled '${candidate.image}' (${candidate.source}), digest=${digest ?? 'unknown'}`);
|
|
206
|
-
return { image: candidate.image, source: candidate.source, digest, pulled: true, attempts };
|
|
302
|
+
return { image: candidate.image, reference: reference(candidate), source: candidate.source, digest, pulled: true, attempts };
|
|
207
303
|
}
|
|
208
304
|
|
|
209
305
|
const error = new Error(describeFailure(attempts));
|
|
@@ -216,6 +312,8 @@ export default {
|
|
|
216
312
|
FORMAL_AI_IMAGE_SOURCES,
|
|
217
313
|
classifyDockerRegistryError,
|
|
218
314
|
ensureFormalAiSidecarImage,
|
|
315
|
+
readAcceptedFormalAiImage,
|
|
316
|
+
resolveAcceptedFormalAiImageCandidates,
|
|
219
317
|
resolveFormalAiFallbackImage,
|
|
220
318
|
resolveFormalAiSidecarImage,
|
|
221
319
|
resolveFormalAiSidecarImageCandidates,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* @see https://github.com/link-assistant/hive-mind/issues/2146
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { buildFormalAiSidecarProvenanceEnv } from './formal-ai-runtime.lib.mjs';
|
|
19
20
|
import { acquireFormalAiSidecar, attachTaskToFormalAiNetwork, isFormalAiSidecarEnabled, isFormalAiTask, releaseFormalAiSidecar } from './formal-ai-sidecar.lib.mjs';
|
|
20
21
|
|
|
21
22
|
const logToConsole = message => console.log(message);
|
|
@@ -43,6 +44,19 @@ export const acquireFormalAiSidecarForTask = async ({ backend, args = [], model
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The environment a Formal AI task is launched with.
|
|
49
|
+
*
|
|
50
|
+
* Besides the endpoint, this carries the identity of the image the lease pinned
|
|
51
|
+
* (issue #2208). The runtime inside the container still asks the endpoint which
|
|
52
|
+
* release is answering; these values let it refuse when the answer is not the
|
|
53
|
+
* accepted image Hive Mind leased, and let the task record the digest rather
|
|
54
|
+
* than a mutable tag.
|
|
55
|
+
*
|
|
56
|
+
* @returns {object} The task environment (`env` itself when Formal AI is not involved).
|
|
57
|
+
*/
|
|
58
|
+
export const buildFormalAiTaskEnv = ({ sidecar, env = process.env } = {}) => (sidecar ? { ...env, HIVE_MIND_FORMAL_AI_BASE_URL: sidecar.baseUrl, ...buildFormalAiSidecarProvenanceEnv(sidecar) } : env);
|
|
59
|
+
|
|
46
60
|
/**
|
|
47
61
|
* Add the internal Formal AI network to the freshly-created task container.
|
|
48
62
|
*
|
|
@@ -66,4 +80,4 @@ export const releaseFormalAiSidecarForTask = async ({ sidecar, sessionId, env =
|
|
|
66
80
|
}
|
|
67
81
|
};
|
|
68
82
|
|
|
69
|
-
export default { acquireFormalAiSidecarForTask, attachFormalAiTaskContainer, releaseFormalAiSidecarForTask };
|
|
83
|
+
export default { acquireFormalAiSidecarForTask, attachFormalAiTaskContainer, buildFormalAiTaskEnv, releaseFormalAiSidecarForTask };
|