@link-assistant/hive-mind 2.14.0 → 2.15.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 +17 -0
- package/package.json +1 -1
- package/src/agent-commander.lib.mjs +8 -0
- package/src/agent-memory-policy.lib.mjs +305 -0
- package/src/claude-quiet-config.lib.mjs +7 -2
- package/src/codex.lib.mjs +7 -0
- package/src/gemini.lib.mjs +5 -0
- package/src/qwen.lib.mjs +4 -0
- package/src/solve.config.lib.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 29612e7: Turn off every AI tool's own cross-task memory and Claude Code's auto-mode classifier by default (issue #2178).
|
|
8
|
+
|
|
9
|
+
A hive-mind task is a disposable container that opens one pull request. The repository — commits, issues, pull requests, case studies — is meant to be the only memory it keeps, because it is the only memory a reviewer can see, correct or revert. Every agentic CLI has since grown a private cross-session store that works against that, and the largest of them (Gemini CLI's auto-memory) is a whole second agent re-reading past sessions on a second model.
|
|
10
|
+
|
|
11
|
+
`--agent-memory-disabled` is new and defaults to `true`. It applies, per tool:
|
|
12
|
+
|
|
13
|
+
- **claude** — `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`, `CLAUDE_CODE_DISABLE_ORG_MEMORY=1`, `autoMemoryEnabled: false`, and `permissions.disableAutoMode: "disable"`. The last one is what removes the classifier: auto mode is what pays for it, and the settings gate is checked before the provider and model gates, so it holds everywhere the environment-variable opt-in would not. Tasks already run `--dangerously-skip-permissions` inside a disposable container, so a classifier deciding whether an action is safe is answering a question that has already been answered.
|
|
14
|
+
- **codex** — `-c features.memories=false -c features.external_agent_memory_import=false`, on both the `codex exec` path and the `--use-agent-commander` path. Both default to off upstream today, but `memories` is a stable-stage flag, so pinning it per run is what makes the default actually hold.
|
|
15
|
+
- **gemini, qwen** — `tools.exclude: ["save_memory"]` and `experimental.autoMemory: false`, merged into the tool's settings file without disturbing anything else already there.
|
|
16
|
+
- **opencode, agent** — no cross-session memory feature was found in either; recorded explicitly so the claim can be re-checked rather than assumed.
|
|
17
|
+
|
|
18
|
+
`--no-agent-memory-disabled` opts out for codex, gemini and qwen, and when it does the policy adds no arguments at all rather than arguments set to `true`. It does not reach claude: those switches are `ENV` lines in the Docker image and settings written by `configure-claude`, neither of which sees a `solve` argv, so they stay off either way and the flag's description says so.
|
|
19
|
+
|
|
3
20
|
## 2.14.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import { detectUsageLimit } from './usage-limit.lib.mjs';
|
|
|
14
14
|
import { applyFormalAiPricingOverride } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
15
15
|
import { getCacheReadTokenCount, getCumulativeContextInputTokens, getOutputTokenCount } from './context-fill.lib.mjs';
|
|
16
16
|
import { ensureAiToolScratchIgnored, filterAiToolScratchFromStatus } from './ai-tool-scratch.lib.mjs';
|
|
17
|
+
import { CLAUDE_MEMORY_DISABLE_ENV, buildCodexMemoryDisableConfigArgs, isAgentMemoryDisabled } from './agent-memory-policy.lib.mjs'; // Issue #2178
|
|
17
18
|
|
|
18
19
|
export const AGENT_COMMANDER_TOOLS = new Set(['claude', 'codex', 'opencode', 'agent', 'qwen', 'gemini']);
|
|
19
20
|
|
|
@@ -57,6 +58,12 @@ const buildClaudeToolOptions = (argv = {}) => {
|
|
|
57
58
|
if (argv.showThinkingContent) extraEnv.CLAUDE_CODE_SHOW_THINKING = '1';
|
|
58
59
|
if (argv.planModel) extraEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = argv.planModel;
|
|
59
60
|
if (argv.subAgentModel) extraEnv.CLAUDE_CODE_SUBAGENT_MODEL = mapClaudeSubAgentModelToEnvValue(argv.subAgentModel);
|
|
61
|
+
// Issue #2178: agent-commander spawns `claude` itself, so it needs the memory
|
|
62
|
+
// opt-out in its own env rather than relying on the settings file alone. Not
|
|
63
|
+
// gated on --agent-memory-disabled: for claude these switches are part of the
|
|
64
|
+
// quiet configuration baked into the image, and the flag governs the tools
|
|
65
|
+
// whose memory is applied per run (codex, gemini, qwen).
|
|
66
|
+
Object.assign(extraEnv, CLAUDE_MEMORY_DISABLE_ENV);
|
|
60
67
|
appendExtraEnv(options, extraEnv);
|
|
61
68
|
|
|
62
69
|
return options;
|
|
@@ -73,6 +80,7 @@ const buildCodexToolOptions = (argv = {}) => {
|
|
|
73
80
|
appendExtraArgs(options, reasoningArgs);
|
|
74
81
|
|
|
75
82
|
appendExtraArgs(options, buildCodexDisable1mContextConfigArgs(!!argv.disable1mContext));
|
|
83
|
+
appendExtraArgs(options, buildCodexMemoryDisableConfigArgs(isAgentMemoryDisabled(argv))); // Issue #2178
|
|
76
84
|
try {
|
|
77
85
|
appendExtraArgs(options, buildCodexSubSessionSizeConfigArgs(parseSubSessionSize(argv.subSessionSize)));
|
|
78
86
|
} catch {
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cross-task memory is off for every agentic CLI hive-mind drives (issue #2178).
|
|
4
|
+
*
|
|
5
|
+
* A hive-mind task is a one-shot: a container is created, an issue is solved, a
|
|
6
|
+
* pull request is opened, the container is destroyed. Nothing an agent learns in
|
|
7
|
+
* one task is meant to reach the next one. The repository is the memory — commits,
|
|
8
|
+
* issues, pull requests, `docs/case-studies/` — and it is the only memory a
|
|
9
|
+
* reviewer can see, correct, or revert.
|
|
10
|
+
*
|
|
11
|
+
* Every agentic CLI now ships some form of private cross-session memory that
|
|
12
|
+
* works against that. It is not free:
|
|
13
|
+
*
|
|
14
|
+
* - it burns inference on top of the task (Gemini CLI runs a whole background
|
|
15
|
+
* extraction agent over past sessions; Claude Code loads a memory index into
|
|
16
|
+
* every session);
|
|
17
|
+
* - it carries facts between unrelated repositories with no review step;
|
|
18
|
+
* - it makes a run irreproducible — the same prompt on the same commit behaves
|
|
19
|
+
* differently depending on what the tool happened to remember.
|
|
20
|
+
*
|
|
21
|
+
* The permission classifiers are the same kind of waste for the same reason.
|
|
22
|
+
* Claude Code's "auto" mode pays a classifier call per tool use to decide whether
|
|
23
|
+
* an action is safe. Hive-mind tasks already run with unrestricted access inside a
|
|
24
|
+
* disposable Docker container (`--dangerously-skip-permissions`,
|
|
25
|
+
* `--dangerously-bypass-approvals-and-sandbox`, `--approval-mode yolo`), so the
|
|
26
|
+
* classifier can only ever answer a question nobody asked.
|
|
27
|
+
*
|
|
28
|
+
* This module is the single place that knows which knob turns each of these off,
|
|
29
|
+
* so `solve` and the Docker image baseline stay in agreement and a new tool
|
|
30
|
+
* version cannot quietly re-enable one of them without a test noticing.
|
|
31
|
+
*
|
|
32
|
+
* Verified against claude-code 2.1.246, codex-cli 0.148.0, gemini-cli 0.51.0,
|
|
33
|
+
* qwen-code 0.7.1 and opencode 1.18.5; see `docs/case-studies/issue-2178/`.
|
|
34
|
+
*
|
|
35
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2178
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fsPromises from 'node:fs/promises';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
|
|
42
|
+
/** Tools `solve --tool` accepts that this policy has something to say about. */
|
|
43
|
+
export const AGENT_MEMORY_POLICY_TOOLS = Object.freeze(['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agent']);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Claude Code environment variables that switch off every cross-session memory
|
|
47
|
+
* store it can reach.
|
|
48
|
+
*
|
|
49
|
+
* `CLAUDE_CODE_DISABLE_AUTO_MEMORY` disables the per-project memory directory,
|
|
50
|
+
* and the same branch also refuses the team stores named by
|
|
51
|
+
* `CLAUDE_MEMORY_STORES`. `CLAUDE_CODE_DISABLE_ORG_MEMORY` disables the
|
|
52
|
+
* organization-wide memory sync, which is gated separately.
|
|
53
|
+
*/
|
|
54
|
+
export const CLAUDE_MEMORY_DISABLE_ENV = Object.freeze({
|
|
55
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
|
|
56
|
+
CLAUDE_CODE_DISABLE_ORG_MEMORY: '1',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Claude Code settings that switch off memory.
|
|
61
|
+
*
|
|
62
|
+
* `autoMemoryEnabled: false` is the settings-file equivalent of
|
|
63
|
+
* `CLAUDE_CODE_DISABLE_AUTO_MEMORY` — both are checked, and either one is
|
|
64
|
+
* enough, so they are set together rather than one being trusted alone.
|
|
65
|
+
*/
|
|
66
|
+
export const CLAUDE_MEMORY_DISABLE_SETTINGS = Object.freeze({
|
|
67
|
+
autoMemoryEnabled: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Claude Code `permissions` block that removes auto mode.
|
|
72
|
+
*
|
|
73
|
+
* `"disable"` is the only value the setting accepts, and it is checked before
|
|
74
|
+
* the plan-level, provider-level and model-level gates, so it holds regardless
|
|
75
|
+
* of which provider or model a task runs on. Auto mode is what pays for the
|
|
76
|
+
* classifier; with it gone the classifier has no caller.
|
|
77
|
+
*/
|
|
78
|
+
export const CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS = Object.freeze({
|
|
79
|
+
disableAutoMode: 'disable',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Codex feature flags that hold its memory subsystem off.
|
|
84
|
+
*
|
|
85
|
+
* `memories` is the `~/.codex/memories` store Codex writes across sessions;
|
|
86
|
+
* `external_agent_memory_import` pulls another agent's memory in. Both default
|
|
87
|
+
* to off today, but `memories` is stage `stable`, so it can be switched on by a
|
|
88
|
+
* rollout or by an operator's `~/.codex/config.toml` — pinning them per run is
|
|
89
|
+
* what makes the default actually hold.
|
|
90
|
+
*/
|
|
91
|
+
export const CODEX_MEMORY_DISABLE_FEATURES = Object.freeze(['memories', 'external_agent_memory_import']);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `-c key=value` overrides that turn {@link CODEX_MEMORY_DISABLE_FEATURES} off.
|
|
95
|
+
*
|
|
96
|
+
* `-c features.<name>=false` is used rather than the `--disable <name>` alias
|
|
97
|
+
* because `--disable` is documented as exactly equivalent and `-c` is accepted
|
|
98
|
+
* by every `codex exec` version hive-mind supports.
|
|
99
|
+
*
|
|
100
|
+
* @param {boolean} [disabled=true] - false returns [] so `--no-agent-memory-disabled` is a no-op.
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
export const buildCodexMemoryDisableConfigArgs = (disabled = true) => {
|
|
104
|
+
if (!disabled) return [];
|
|
105
|
+
return CODEX_MEMORY_DISABLE_FEATURES.flatMap(feature => ['-c', `features.${feature}=false`]);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tool name (as the CLI exposes it) of the Gemini-family memory writer.
|
|
110
|
+
*
|
|
111
|
+
* Gemini CLI dropped `save_memory` in 0.51 in favour of the `experimental.autoMemory`
|
|
112
|
+
* background extractor; Qwen Code, forked earlier, still ships the tool. Excluding
|
|
113
|
+
* the name covers the versions that have it and is inert on the versions that do not.
|
|
114
|
+
*/
|
|
115
|
+
export const GEMINI_FAMILY_MEMORY_TOOL = 'save_memory';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Settings merged into `~/.gemini/settings.json` and `~/.qwen/settings.json`.
|
|
119
|
+
*
|
|
120
|
+
* `tools.exclude` is the nested form both CLIs resolve to (Qwen maps its legacy
|
|
121
|
+
* flat `excludeTools` onto it). `experimental.autoMemory` gates Gemini's
|
|
122
|
+
* background "skill extraction" agent, which re-reads past sessions with a second
|
|
123
|
+
* model and writes a memory index — the most expensive item on this list.
|
|
124
|
+
*/
|
|
125
|
+
export const GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS = Object.freeze({
|
|
126
|
+
tools: Object.freeze({ exclude: Object.freeze([GEMINI_FAMILY_MEMORY_TOOL]) }),
|
|
127
|
+
experimental: Object.freeze({ autoMemory: false }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/** Where each Gemini-family CLI keeps its user settings. */
|
|
131
|
+
export const GEMINI_FAMILY_SETTINGS_PATHS = Object.freeze({
|
|
132
|
+
gemini: Object.freeze(['.gemini', 'settings.json']),
|
|
133
|
+
qwen: Object.freeze(['.qwen', 'settings.json']),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tools with no cross-session memory feature to switch off.
|
|
138
|
+
*
|
|
139
|
+
* Recorded explicitly rather than left out: "we looked and there was nothing" and
|
|
140
|
+
* "nobody looked" are different states, and only the first one stays true without
|
|
141
|
+
* someone re-checking. The test for this module asserts the list, so a future
|
|
142
|
+
* reader can see the claim was made deliberately.
|
|
143
|
+
*/
|
|
144
|
+
export const TOOLS_WITHOUT_MEMORY_FEATURE = Object.freeze(['opencode', 'agent']);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Is the policy on for this run?
|
|
148
|
+
*
|
|
149
|
+
* Reads `--agent-memory-disabled`, which defaults to true. Only an explicit
|
|
150
|
+
* `--no-agent-memory-disabled` turns it off, so an argv object that predates the
|
|
151
|
+
* flag (or omits it) still gets the policy.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} [argv]
|
|
154
|
+
*/
|
|
155
|
+
export const isAgentMemoryDisabled = (argv = {}) => argv?.agentMemoryDisabled !== false;
|
|
156
|
+
|
|
157
|
+
const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Merge `desired` into `target` in place, returning the dotted paths that changed.
|
|
161
|
+
*
|
|
162
|
+
* Arrays are unioned rather than replaced so an operator's own `tools.exclude`
|
|
163
|
+
* entries survive; scalars are overwritten, because the whole point is that the
|
|
164
|
+
* policy wins.
|
|
165
|
+
*
|
|
166
|
+
* `__proto__`, `constructor` and `prototype` are skipped outright. Today this
|
|
167
|
+
* function is only ever handed {@link GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS}, a
|
|
168
|
+
* frozen literal whose keys are all ordinary, so none of them can occur — but
|
|
169
|
+
* that is a fact about the caller, not about the function, and callers change
|
|
170
|
+
* (CodeQL `js/prototype-pollution-utility`). The guard is written as explicit
|
|
171
|
+
* comparisons rather than a lookup in a shared set because that is the shape the
|
|
172
|
+
* query recognises as a barrier, and a guard a scanner cannot see is one that
|
|
173
|
+
* gets reported again every time someone touches the file.
|
|
174
|
+
*/
|
|
175
|
+
const mergeSettings = (target, desired, prefix = '') => {
|
|
176
|
+
const changed = [];
|
|
177
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
178
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
179
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
180
|
+
if (isPlainObject(value)) {
|
|
181
|
+
if (!isPlainObject(target[key])) target[key] = {};
|
|
182
|
+
changed.push(...mergeSettings(target[key], value, dotted));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
const existing = Array.isArray(target[key]) ? target[key] : [];
|
|
187
|
+
const merged = [...existing];
|
|
188
|
+
let added = false;
|
|
189
|
+
for (const entry of value) {
|
|
190
|
+
if (!merged.includes(entry)) {
|
|
191
|
+
merged.push(entry);
|
|
192
|
+
added = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (added || !Array.isArray(target[key])) {
|
|
196
|
+
target[key] = merged;
|
|
197
|
+
changed.push(dotted);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (target[key] !== value) {
|
|
202
|
+
target[key] = value;
|
|
203
|
+
changed.push(dotted);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return changed;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve the settings file a Gemini-family CLI reads.
|
|
211
|
+
*
|
|
212
|
+
* @param {'gemini'|'qwen'} tool
|
|
213
|
+
* @param {Object} [options]
|
|
214
|
+
* @param {string} [options.homeDir]
|
|
215
|
+
* @returns {string|null} null when the tool has no Gemini-family settings file.
|
|
216
|
+
*/
|
|
217
|
+
export const resolveGeminiFamilySettingsPath = (tool, { homeDir = os.homedir() } = {}) => {
|
|
218
|
+
const segments = GEMINI_FAMILY_SETTINGS_PATHS[tool];
|
|
219
|
+
if (!segments) return null;
|
|
220
|
+
return path.join(homeDir, ...segments);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Write {@link GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS} into a Gemini-family
|
|
225
|
+
* settings file, preserving everything already there.
|
|
226
|
+
*
|
|
227
|
+
* Never throws. A task that cannot write the settings file is still a task worth
|
|
228
|
+
* running: the failure costs inference, not correctness, and the caller logs it.
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} [params]
|
|
231
|
+
* @param {'gemini'|'qwen'} params.tool
|
|
232
|
+
* @param {string} [params.settingsPath] - Overrides the tool's default location (tests).
|
|
233
|
+
* @param {string} [params.homeDir]
|
|
234
|
+
* @param {Function} [params.log]
|
|
235
|
+
* @param {Object} [params.fsImpl] - `node:fs/promises`-shaped, for tests.
|
|
236
|
+
* @returns {Promise<{applied: boolean, path: string|null, changed: string[], error: string|null}>}
|
|
237
|
+
*/
|
|
238
|
+
export const ensureGeminiFamilyMemoryDisabled = async ({ tool, settingsPath, homeDir = os.homedir(), log, fsImpl = fsPromises } = {}) => {
|
|
239
|
+
const resolvedPath = settingsPath || resolveGeminiFamilySettingsPath(tool, { homeDir });
|
|
240
|
+
if (!resolvedPath) return { applied: false, path: null, changed: [], error: null };
|
|
241
|
+
|
|
242
|
+
let settings = {};
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(await fsImpl.readFile(resolvedPath, 'utf-8'));
|
|
245
|
+
if (isPlainObject(parsed)) settings = parsed;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error?.code !== 'ENOENT' && log) {
|
|
248
|
+
await log(`⚠️ Could not read ${resolvedPath}: ${error.message}`, { verbose: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const changed = mergeSettings(settings, GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS);
|
|
253
|
+
try {
|
|
254
|
+
if (changed.length > 0) {
|
|
255
|
+
await fsImpl.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
256
|
+
await fsImpl.writeFile(resolvedPath, JSON.stringify(settings, null, 2));
|
|
257
|
+
}
|
|
258
|
+
if (log) {
|
|
259
|
+
await log(`🧠 Cross-task memory ${changed.length > 0 ? 'disabled' : 'already disabled'} for ${tool} in ${resolvedPath} (issue #2178)`, { verbose: true });
|
|
260
|
+
}
|
|
261
|
+
return { applied: true, path: resolvedPath, changed, error: null };
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const message = error?.message || String(error);
|
|
264
|
+
if (log) await log(`⚠️ Could not write ${resolvedPath}: ${message}`, { verbose: true });
|
|
265
|
+
return { applied: false, path: resolvedPath, changed: [], error: message };
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* One line describing what the policy does for a tool, for `--verbose` logs and
|
|
271
|
+
* for the docs to quote without drifting from the code.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} tool
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
export const describeAgentMemoryPolicy = tool => {
|
|
277
|
+
switch (tool) {
|
|
278
|
+
case 'claude':
|
|
279
|
+
return `settings ${JSON.stringify({ ...CLAUDE_MEMORY_DISABLE_SETTINGS, permissions: CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS })}, env ${Object.keys(CLAUDE_MEMORY_DISABLE_ENV).join(', ')}`;
|
|
280
|
+
case 'codex':
|
|
281
|
+
return buildCodexMemoryDisableConfigArgs(true).join(' ');
|
|
282
|
+
case 'gemini':
|
|
283
|
+
case 'qwen':
|
|
284
|
+
return `settings ${JSON.stringify(GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS)}`;
|
|
285
|
+
default:
|
|
286
|
+
return TOOLS_WITHOUT_MEMORY_FEATURE.includes(tool) ? 'no cross-session memory feature to disable' : 'no policy recorded for this tool';
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export default {
|
|
291
|
+
AGENT_MEMORY_POLICY_TOOLS,
|
|
292
|
+
CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS,
|
|
293
|
+
CLAUDE_MEMORY_DISABLE_ENV,
|
|
294
|
+
CLAUDE_MEMORY_DISABLE_SETTINGS,
|
|
295
|
+
CODEX_MEMORY_DISABLE_FEATURES,
|
|
296
|
+
GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS,
|
|
297
|
+
GEMINI_FAMILY_MEMORY_TOOL,
|
|
298
|
+
GEMINI_FAMILY_SETTINGS_PATHS,
|
|
299
|
+
TOOLS_WITHOUT_MEMORY_FEATURE,
|
|
300
|
+
buildCodexMemoryDisableConfigArgs,
|
|
301
|
+
describeAgentMemoryPolicy,
|
|
302
|
+
ensureGeminiFamilyMemoryDisabled,
|
|
303
|
+
isAgentMemoryDisabled,
|
|
304
|
+
resolveGeminiFamilySettingsPath,
|
|
305
|
+
};
|
|
@@ -4,8 +4,12 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
+
// Issue #2178: memory and auto mode are governed by one cross-tool policy module
|
|
8
|
+
// so `solve` cannot disagree with the Docker image baseline about what "off" means.
|
|
9
|
+
import { CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS, CLAUDE_MEMORY_DISABLE_ENV, CLAUDE_MEMORY_DISABLE_SETTINGS } from './agent-memory-policy.lib.mjs';
|
|
10
|
+
|
|
7
11
|
export const REQUIRED_CLAUDE_QUIET_ENV = Object.freeze({
|
|
8
|
-
|
|
12
|
+
...CLAUDE_MEMORY_DISABLE_ENV,
|
|
9
13
|
CLAUDE_CODE_DISABLE_CRON: '1',
|
|
10
14
|
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: '1',
|
|
11
15
|
CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1',
|
|
@@ -20,7 +24,7 @@ export const REQUIRED_CLAUDE_QUIET_ENV = Object.freeze({
|
|
|
20
24
|
});
|
|
21
25
|
|
|
22
26
|
export const REQUIRED_CLAUDE_QUIET_SETTINGS = Object.freeze({
|
|
23
|
-
|
|
27
|
+
...CLAUDE_MEMORY_DISABLE_SETTINGS,
|
|
24
28
|
spinnerTipsEnabled: false,
|
|
25
29
|
awaySummaryEnabled: false,
|
|
26
30
|
feedbackSurveyRate: 0,
|
|
@@ -38,6 +42,7 @@ export const REQUIRED_CLAUDE_QUIET_ATTRIBUTION = Object.freeze({
|
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
export const REQUIRED_CLAUDE_QUIET_PERMISSIONS = Object.freeze({
|
|
45
|
+
...CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS,
|
|
41
46
|
defaultMode: 'bypassPermissions',
|
|
42
47
|
});
|
|
43
48
|
|
package/src/codex.lib.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import { buildAuthRemedyLines, buildFormalAiEnvExports, isPrepareOnly, logPrepar
|
|
|
40
40
|
import { buildFormalAiPricingInfo } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
41
41
|
import { classifyRetryableError, createTransientRetryBudget, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
42
42
|
import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
43
|
+
import { buildCodexMemoryDisableConfigArgs, isAgentMemoryDisabled } from './agent-memory-policy.lib.mjs'; // Issue #2178
|
|
43
44
|
import { getCumulativeContextInputTokens } from './context-fill.lib.mjs';
|
|
44
45
|
import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
|
|
45
46
|
import { applyCodexCapabilityEnv, runCodexCapabilityPreflight } from './codex-capability-preflight.lib.mjs'; // Issue #2074
|
|
@@ -708,9 +709,15 @@ export const executeCodexCommand = async params => {
|
|
|
708
709
|
for (const arg of subSessionSizeArgs) {
|
|
709
710
|
codexArgs += ` ${shellQuote(arg)}`;
|
|
710
711
|
}
|
|
712
|
+
// Issue #2178: a hive-mind task must not remember anything a reviewer cannot see.
|
|
713
|
+
const memoryDisableArgs = buildCodexMemoryDisableConfigArgs(isAgentMemoryDisabled(argv));
|
|
714
|
+
for (const arg of memoryDisableArgs) {
|
|
715
|
+
codexArgs += ` ${shellQuote(arg)}`;
|
|
716
|
+
}
|
|
711
717
|
if (argv.verbose) {
|
|
712
718
|
if (disable1mArgs.length) await log(`📊 Codex --disable-1m-context: ${disable1mArgs.join(' ')}`, { verbose: true });
|
|
713
719
|
if (subSessionSizeArgs.length) await log(`📊 Codex --sub-session-size: ${subSessionSizeArgs.join(' ')}`, { verbose: true });
|
|
720
|
+
if (memoryDisableArgs.length) await log(`🧠 Codex cross-task memory disabled: ${memoryDisableArgs.join(' ')} (issue #2178)`, { verbose: true });
|
|
714
721
|
}
|
|
715
722
|
// Issue #2130: re-export the Formal AI environment inside the `sh -lc` script so a
|
|
716
723
|
// stale `formal-ai with --global` block in the operator profile cannot override it.
|
package/src/gemini.lib.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { getCumulativeContextInputTokens, toTokenCount } from './context-fill.li
|
|
|
31
31
|
import { ensureAiToolScratchIgnored, filterAiToolScratchFromStatus } from './ai-tool-scratch.lib.mjs';
|
|
32
32
|
import { getTerminalEventCompletionHealth } from './tool-run-health.lib.mjs'; // Issue #1990
|
|
33
33
|
import { takeJsonRecords } from './json-stream.lib.mjs'; // Issue #2119
|
|
34
|
+
import { ensureGeminiFamilyMemoryDisabled, isAgentMemoryDisabled } from './agent-memory-policy.lib.mjs'; // Issue #2178
|
|
34
35
|
|
|
35
36
|
const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
36
37
|
|
|
@@ -421,6 +422,10 @@ export const executeGeminiCommand = async params => {
|
|
|
421
422
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
422
423
|
|
|
423
424
|
const mappedModel = mapModelToId(argv.model || defaultModels.gemini);
|
|
425
|
+
// Issue #2178: the repository is the only memory a hive-mind task keeps, so
|
|
426
|
+
// `save_memory` and the background auto-memory extractor are switched off
|
|
427
|
+
// before the CLI reads its settings.
|
|
428
|
+
if (isAgentMemoryDisabled(argv)) await ensureGeminiFamilyMemoryDisabled({ tool: 'gemini', log });
|
|
424
429
|
// Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
|
|
425
430
|
const toolInvocation = await resolveFormalAiToolExecution({ tool: 'gemini', model: argv.model || defaultModels.gemini, toolPath: geminiPath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv) });
|
|
426
431
|
const geminiEnv = { ...process.env, ...toolInvocation.env };
|
package/src/qwen.lib.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { ensureAiToolScratchIgnored, filterAiToolScratchFromStatus } from './ai-
|
|
|
32
32
|
import { getTerminalEventCompletionHealth } from './tool-run-health.lib.mjs'; // Issue #1990
|
|
33
33
|
import { takeJsonRecords } from './json-stream.lib.mjs'; // Issue #2119
|
|
34
34
|
import { stringifyErrorValue } from './error-text.lib.mjs'; // Issue #2141
|
|
35
|
+
import { ensureGeminiFamilyMemoryDisabled, isAgentMemoryDisabled } from './agent-memory-policy.lib.mjs'; // Issue #2178
|
|
35
36
|
|
|
36
37
|
export const mapModelToId = model => qwenModels[model] || model;
|
|
37
38
|
|
|
@@ -524,6 +525,9 @@ export const executeQwenCommand = async params => {
|
|
|
524
525
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
525
526
|
|
|
526
527
|
const mappedModel = mapModelToId(argv.model || defaultModels.qwen);
|
|
528
|
+
// Issue #2178: Qwen Code still ships the `save_memory` tool it inherited from
|
|
529
|
+
// Gemini CLI. Exclude it so nothing this task learns outlives the container.
|
|
530
|
+
if (isAgentMemoryDisabled(argv)) await ensureGeminiFamilyMemoryDisabled({ tool: 'qwen', log });
|
|
527
531
|
// Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
|
|
528
532
|
const toolInvocation = await resolveFormalAiToolExecution({ tool: 'qwen', model: argv.model || defaultModels.qwen, toolPath: qwenPath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv) });
|
|
529
533
|
const qwenEnv = { ...process.env, ...toolInvocation.env };
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -603,6 +603,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
603
603
|
description: 'Disable Claude Code built-in tools and MCP servers that have no value (and may be harmful) in autonomous headless runs: AskUserQuestion, CronCreate/Delete/List, EnterPlanMode/ExitPlanMode, EnterWorktree/ExitWorktree, Monitor, NotebookEdit, PushNotification, RemoteTrigger, ScheduleWakeup, and the claude.ai Gmail/Drive/Calendar OAuth connectors. Default: true. Use --no-useless-tools-disabled to keep them enabled. Supported for --tool claude (issue #1627).',
|
|
604
604
|
default: true,
|
|
605
605
|
},
|
|
606
|
+
'agent-memory-disabled': {
|
|
607
|
+
type: 'boolean',
|
|
608
|
+
description: "Disable every AI tool's own cross-task memory and permission classifier, so the repository stays the only memory a task keeps. For --tool codex: -c features.memories=false and -c features.external_agent_memory_import=false. For --tool gemini and --tool qwen: tools.exclude=[save_memory] and experimental.autoMemory=false. --tool opencode and --tool agent have no cross-session memory feature. Default: true; --no-agent-memory-disabled lets those tools keep their own memory. For --tool claude the same switches (CLAUDE_CODE_DISABLE_AUTO_MEMORY, CLAUDE_CODE_DISABLE_ORG_MEMORY, autoMemoryEnabled=false, permissions.disableAutoMode=disable) are part of the quiet configuration baked into the Docker image, so they stay off regardless of this flag (issue #2178).",
|
|
609
|
+
default: true,
|
|
610
|
+
},
|
|
606
611
|
'auto-gh-configuration-repair': {
|
|
607
612
|
type: 'boolean',
|
|
608
613
|
description: 'Automatically repair git configuration using gh-setup-git-identity --repair when git identity is not configured. Requires gh-setup-git-identity to be installed.',
|