@link-assistant/hive-mind 2.11.13 → 2.12.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 +10 -0
- package/package.json +1 -1
- package/src/agent-command.lib.mjs +74 -0
- package/src/agent.lib.mjs +59 -34
- package/src/agentic-cli-updater.lib.mjs +241 -0
- package/src/formal-ai-isolation.lib.mjs +62 -0
- package/src/formal-ai-maintenance.lib.mjs +106 -0
- package/src/formal-ai-model.lib.mjs +25 -0
- package/src/formal-ai-runtime.lib.mjs +10 -0
- package/src/formal-ai-sidecar.lib.mjs +565 -0
- package/src/formal-ai-updater.lib.mjs +294 -0
- package/src/formal-ai-version.lib.mjs +100 -0
- package/src/formal-ai.lib.mjs +11 -16
- package/src/isolation-runner.lib.mjs +32 -1
- package/src/model-args.lib.mjs +32 -0
- package/src/models/index.mjs +5 -4
- package/src/solve.results.lib.mjs +2 -2
- package/src/state-lock.lib.mjs +82 -0
- package/src/telegram-bot.mjs +18 -0
- package/src/telegram-fix-command.lib.mjs +1 -8
- package/src/telegram-task-command.lib.mjs +1 -8
- package/src/working-session-summary.lib.mjs +47 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background maintenance for the Formal AI sidecar and the agentic CLIs
|
|
3
|
+
* (issue #2146, PR #2147 review).
|
|
4
|
+
*
|
|
5
|
+
* One periodic tick performs the three idle-only duties the review asked for,
|
|
6
|
+
* in the only order that is safe:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Reconcile and stop.** Drop leases whose task container is gone and, if
|
|
9
|
+
* nothing is left, stop the sidecar. Must run first — the update and the
|
|
10
|
+
* CLI refresh both require an idle host, and a crashed task would
|
|
11
|
+
* otherwise keep the sidecar alive forever.
|
|
12
|
+
* 2. **Update the Formal AI image**, including the non-destructive memory
|
|
13
|
+
* migration, while the sidecar is stopped.
|
|
14
|
+
* 3. **Refresh the agentic CLIs**, which is throttled independently because
|
|
15
|
+
* it queries the npm registry.
|
|
16
|
+
*
|
|
17
|
+
* Every step is best-effort: maintenance must never take the bot down, and a
|
|
18
|
+
* failure is reported and retried on the next tick rather than thrown.
|
|
19
|
+
*
|
|
20
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2146
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { updateAgenticClisWhenIdle } from './agentic-cli-updater.lib.mjs';
|
|
24
|
+
import { reconcileFormalAiSidecar, stopFormalAiSidecar, withFormalAiSidecarLock } from './formal-ai-sidecar.lib.mjs';
|
|
25
|
+
import { updateFormalAiSidecarWhenIdle } from './formal-ai-updater.lib.mjs';
|
|
26
|
+
|
|
27
|
+
/** Default gap between maintenance ticks. */
|
|
28
|
+
export const DEFAULT_FORMAL_AI_MAINTENANCE_INTERVAL_MS = 5 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stop the sidecar when no Formal AI task holds a lease any more.
|
|
32
|
+
*
|
|
33
|
+
* This is what turns "the task finished" into "the container is gone" without
|
|
34
|
+
* having to hook every completion path: a lease is only live while its task
|
|
35
|
+
* container is, so a finished, killed or crashed task all converge here.
|
|
36
|
+
*
|
|
37
|
+
* @returns {Promise<{leaseCount: number, stopped: boolean}>}
|
|
38
|
+
*/
|
|
39
|
+
export const stopIdleFormalAiSidecar = async ({ env = process.env, run, log = null, verbose = false, lockOptions = {} } = {}) =>
|
|
40
|
+
withFormalAiSidecarLock(
|
|
41
|
+
async () => {
|
|
42
|
+
const { leaseCount, container } = await reconcileFormalAiSidecar({ env, run, log, verbose });
|
|
43
|
+
if (leaseCount > 0 || !container.exists) return { leaseCount, stopped: false };
|
|
44
|
+
const { stopped } = await stopFormalAiSidecar({ env, run, log, verbose, reason: 'no Formal AI tasks running' });
|
|
45
|
+
return { leaseCount: 0, stopped };
|
|
46
|
+
},
|
|
47
|
+
{ env, log, ...lockOptions }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run one maintenance tick.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<{idle: object, formalAi: object|null, agenticClis: object|null, errors: object[]}>}
|
|
54
|
+
*/
|
|
55
|
+
export const runFormalAiMaintenanceTick = async ({ env = process.env, run, log = null, verbose = false, updateFormalAi = updateFormalAiSidecarWhenIdle, updateClis = updateAgenticClisWhenIdle, stopIdle = stopIdleFormalAiSidecar } = {}) => {
|
|
56
|
+
const errors = [];
|
|
57
|
+
|
|
58
|
+
let idle = { leaseCount: null, stopped: false };
|
|
59
|
+
try {
|
|
60
|
+
idle = await stopIdle({ env, run, log, verbose });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
errors.push({ stage: 'stop-idle', error: error?.message || String(error) });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let formalAi = null;
|
|
66
|
+
try {
|
|
67
|
+
formalAi = await updateFormalAi({ env, run, log, verbose });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
errors.push({ stage: 'formal-ai-update', error: error?.message || String(error) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let agenticClis = null;
|
|
73
|
+
try {
|
|
74
|
+
agenticClis = await updateClis({ env, run, log, verbose });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
errors.push({ stage: 'agentic-cli-update', error: error?.message || String(error) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (log && errors.length > 0) await log(`⚠️ Formal AI maintenance tick had ${errors.length} problem(s): ${errors.map(entry => `${entry.stage}: ${entry.error}`).join('; ')}`);
|
|
80
|
+
if (verbose && log) await log(`[VERBOSE] formal-ai-maintenance: leases=${idle.leaseCount ?? 'unknown'} stopped=${idle.stopped} update=${formalAi?.status ?? 'skipped'} clis=${agenticClis?.status ?? 'skipped'}`);
|
|
81
|
+
return { idle, formalAi, agenticClis, errors };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start the periodic maintenance timer.
|
|
86
|
+
*
|
|
87
|
+
* The timer is unref'd so it never keeps the process alive on shutdown.
|
|
88
|
+
*
|
|
89
|
+
* @returns {{stop: () => void}}
|
|
90
|
+
*/
|
|
91
|
+
export const startFormalAiMaintenance = ({ env = process.env, log = null, verbose = false, intervalMs = DEFAULT_FORMAL_AI_MAINTENANCE_INTERVAL_MS, setIntervalImpl = setInterval, clearIntervalImpl = clearInterval, runTick = runFormalAiMaintenanceTick } = {}) => {
|
|
92
|
+
const tick = () => {
|
|
93
|
+
runTick({ env, log, verbose }).catch(error => {
|
|
94
|
+
console.error(`[formal-ai-maintenance] tick failed: ${error?.message || error}`);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const timer = setIntervalImpl(tick, intervalMs);
|
|
99
|
+
timer?.unref?.();
|
|
100
|
+
tick();
|
|
101
|
+
return {
|
|
102
|
+
stop: () => clearIntervalImpl(timer),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default { DEFAULT_FORMAL_AI_MAINTENANCE_INTERVAL_MS, runFormalAiMaintenanceTick, startFormalAiMaintenance, stopIdleFormalAiSidecar };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formal AI model identity — the smallest module that can answer "is this task
|
|
3
|
+
* driven by Formal AI?".
|
|
4
|
+
*
|
|
5
|
+
* These three symbols used to live in `src/models/index.mjs`, which bootstraps
|
|
6
|
+
* `use-m` at import time and therefore reaches the network. The Formal AI
|
|
7
|
+
* sidecar lifecycle (issue #2146) is imported by the isolation runner, whose
|
|
8
|
+
* own regression test asserts that importing it performs no network fetch, so
|
|
9
|
+
* the identity check has to be reachable without dragging the whole model
|
|
10
|
+
* catalogue in. `src/models/index.mjs` re-exports these so there is still a
|
|
11
|
+
* single definition.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2146
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The alias users type: `--model formal-ai`. */
|
|
17
|
+
export const FORMAL_AI_MODEL_ALIAS = 'formal-ai';
|
|
18
|
+
|
|
19
|
+
/** The provider-qualified id used when a tool routes through a provider. */
|
|
20
|
+
export const FORMAL_AI_PROVIDER_MODEL_ID = 'formalai/formal-ai';
|
|
21
|
+
|
|
22
|
+
/** True when a model string names Formal AI in either spelling. */
|
|
23
|
+
export const isFormalAiModel = model => model === FORMAL_AI_MODEL_ALIAS || model === FORMAL_AI_PROVIDER_MODEL_ID;
|
|
24
|
+
|
|
25
|
+
export default { FORMAL_AI_MODEL_ALIAS, FORMAL_AI_PROVIDER_MODEL_ID, isFormalAiModel };
|
|
@@ -53,6 +53,8 @@ import { homedir } from 'node:os';
|
|
|
53
53
|
import { dirname, join } from 'node:path';
|
|
54
54
|
import { promisify } from 'node:util';
|
|
55
55
|
|
|
56
|
+
import { assertSupportedFormalAiVersion, FORMAL_AI_MINIMUM_VERSION, readFormalAiBinaryVersion } from './formal-ai-version.lib.mjs';
|
|
57
|
+
|
|
56
58
|
const execFileAsync = promisify(execFile);
|
|
57
59
|
|
|
58
60
|
export const FORMAL_AI_DEFAULT_API_KEY = 'formal-ai';
|
|
@@ -359,6 +361,13 @@ export const prepareFormalAiRuntime = async ({ tool, workdir, log = async () =>
|
|
|
359
361
|
|
|
360
362
|
installExitHook();
|
|
361
363
|
|
|
364
|
+
// Issue #2146: `--no-tool-check` skipped the only version probe, allowing an
|
|
365
|
+
// old Formal AI build to return the same unexecuted plan through all five
|
|
366
|
+
// Claude/Codex restarts. Runtime safety cannot depend on preflight options.
|
|
367
|
+
const formalAiVersion = await (deps.readVersionImpl || readFormalAiBinaryVersion)({ formalAiPath: resolvedFormalAiPath, env });
|
|
368
|
+
assertSupportedFormalAiVersion(formalAiVersion);
|
|
369
|
+
await log(`🧠 Formal AI: version ${formalAiVersion} (minimum ${FORMAL_AI_MINIMUM_VERSION})`);
|
|
370
|
+
|
|
362
371
|
const apiKey = resolveFormalAiApiKey(env);
|
|
363
372
|
const externalBaseUrl = env.HIVE_MIND_FORMAL_AI_BASE_URL?.trim() || null;
|
|
364
373
|
const homeRoot = resolveFormalAiHomeRoot(env);
|
|
@@ -420,6 +429,7 @@ export const prepareFormalAiRuntime = async ({ tool, workdir, log = async () =>
|
|
|
420
429
|
client,
|
|
421
430
|
notes,
|
|
422
431
|
serverStarted: !!server,
|
|
432
|
+
formalAiVersion,
|
|
423
433
|
stop: async () => {
|
|
424
434
|
runtimeCache.delete(cacheKey);
|
|
425
435
|
await server?.stop?.();
|