@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.14.0",
3
+ "version": "2.15.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",
@@ -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
- CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
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
- autoMemoryEnabled: false,
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.
@@ -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 };
@@ -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.',