@link-assistant/hive-mind 2.13.5 → 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,32 @@
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
+
20
+ ## 2.14.0
21
+
22
+ ### Minor Changes
23
+
24
+ - e796988: Add the experimental `--use-router` option, which stops handing a Docker-isolated task the operator's real subscription. With the flag, `~/.claude`, `~/.claude.json`, `~/.codex` and `~/.agents` are mounted only into a reference-counted `hive-mind-router` sidecar (pinned to `ghcr.io/link-assistant/router:0.119.0`) on an internal Docker network; each task is joined to that network and given its own `la_sk_…` token scoped to the one repository it works on, so every model request lands in that token's own redacted log. The sidecar runs while at least one task holds a lease and is stopped when the last one is released, with the Telegram bot reconciling leases against Docker so a killed bot cannot leave it running forever. At end of task the agent's session data is drained into the router's data volume for audit, and that volume is never removed by any code path.
25
+
26
+ GitHub is routed too: the sidecar terminates TLS itself with a certificate covering `api.github.com`, and the task container resolves that name to the router, so an unmodified `gh` is mediated without holding a GitHub credential of its own. `git` pushes through the router's `/git/*` proxy, which refuses remote-ref deletions and, from router `0.110.0`, non-fast-forward updates as well — it asks GitHub's compare API whether the proposed tip is ahead and fails closed when it cannot tell. A read-only `pre-push` hook in the task covers the same ground locally and for non-routed remotes. `--model formal-ai` is served through the router as a stored OpenAI-compatible provider, so it is audited like every other model.
27
+
28
+ Nothing changes without the flag. The remaining limits are documented and printed before every routed run: destructive GitHub API calls are blocked by HTTP method rather than by effect, so `PUT` branch protection, `PUT` rulesets, `POST` transfer and `PATCH` visibility/archived/default_branch are still forwarded (upstream `link-assistant/router#329`) — which puts branch protection itself within reach of a routed token; the Formal AI sidecar's own upstream calls are not routed; and the router resolves exact model ids only, not aliases, so the tier-shaped surface (`--plan`, `--escalate`, fallback chains) needs dated ids on a routed run. New docs: `docs/ROUTER.md`, `docs/COLLECTING-LOGS.md`, `docs/case-studies/issue-2164/` and `examples/collect-logs.mjs`.
29
+
3
30
  ## 2.13.5
4
31
 
5
32
  ### Patch Changes
package/README.hi.md CHANGED
@@ -88,6 +88,8 @@ Hive Mind में औसत प्रोग्रामर से अलग
88
88
 
89
89
  Hive Mind अपने प्रबंधित terminal, log, telemetry, upload और GitHub publication paths में पहचाने गए credentials को sanitize करता है। Scanner विफल होने पर publication रोक दिया जाता है, लेकिन यह किसी autonomous tool को किसी असंबंधित exfiltration path का उपयोग करने से नहीं रोकता। Masking contract, supported formats, maintenance process और incident-response guidance के लिए [Credential sanitization](./docs/CREDENTIAL-SANITIZATION.hi.md) देखें।
90
90
 
91
+ डिफ़ॉल्ट रूप से Docker में आइसोलेट किए गए कार्यों को ऑपरेटर के Claude/Codex क्रेडेंशियल मिल जाते हैं। `--use-router` उन्हें रोक देता है और मॉडल ट्रैफ़िक को `hive-mind-router` sidecar से भेजता है, जिससे हर कार्य को अपना सीमित दायरे वाला टोकन और अपना request लॉग मिलता है — देखें [राउटर आइसोलेशन](./docs/ROUTER.hi.md) (प्रयोगात्मक) और [लॉग एकत्र करना](./docs/COLLECTING-LOGS.hi.md)।
92
+
91
93
  `hive.mjs` चलाने के लिए न्यूनतम सिस्टम आवश्यकताएँ:
92
94
 
93
95
  ```
package/README.md CHANGED
@@ -88,6 +88,8 @@ We strongly recommend:
88
88
 
89
89
  Hive Mind sanitizes recognized credentials in its maintained terminal, log, telemetry, upload, and GitHub publication paths. Publication is blocked if the scanner fails, but this does not prevent an autonomous tool from using an unrelated exfiltration path. See [Credential sanitization](./docs/CREDENTIAL-SANITIZATION.md) for the masking contract, supported formats, maintenance process, and incident-response guidance.
90
90
 
91
+ Docker-isolated tasks receive the operator's Claude/Codex credentials by default. `--use-router` withholds them and routes model traffic through a `hive-mind-router` sidecar instead, giving each task its own scoped token and its own request log — see [Router isolation](./docs/ROUTER.md) (experimental) and [Collecting logs](./docs/COLLECTING-LOGS.md).
92
+
91
93
  Minimum system requirements to run `hive.mjs`:
92
94
 
93
95
  ```
package/README.ru.md CHANGED
@@ -88,6 +88,8 @@ Hive Mind обладает высоким уровнем творчества,
88
88
 
89
89
  Hive Mind скрывает распознанные учётные данные в обслуживаемых им путях вывода терминала, журналов, телеметрии, загрузок и публикаций GitHub. При сбое сканера публикация блокируется, однако это не мешает автономному инструменту использовать посторонний канал утечки. Соглашение о маскировании, поддерживаемые форматы, процесс сопровождения и рекомендации по реагированию на инциденты приведены в разделе [Санитизация учётных данных](./docs/CREDENTIAL-SANITIZATION.ru.md).
90
90
 
91
+ По умолчанию задачи, изолированные в Docker, получают учётные данные Claude/Codex оператора. `--use-router` их не передаёт и вместо этого направляет трафик модели через sidecar `hive-mind-router`, выдавая каждой задаче собственный ограниченный токен и собственный журнал запросов — см. [Изоляция через роутер](./docs/ROUTER.ru.md) (экспериментально) и [Сбор журналов](./docs/COLLECTING-LOGS.ru.md).
92
+
91
93
  Минимальные системные требования для запуска `hive.mjs`:
92
94
 
93
95
  ```
package/README.zh.md CHANGED
@@ -88,6 +88,8 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
88
88
 
89
89
  Hive Mind 会在其维护的终端、日志、遥测、上传和 GitHub 发布路径中清理可识别的凭证。如果扫描器失败,发布会被阻止;但这无法阻止自主工具通过不相关的路径泄露数据。有关掩码约定、支持的格式、维护流程和事件响应指南,请参阅[凭证清理](./docs/CREDENTIAL-SANITIZATION.zh.md)。
90
90
 
91
+ 默认情况下,Docker 隔离的任务会拿到操作者的 Claude/Codex 凭据。`--use-router` 不再传入这些凭据,而是让模型流量经由 `hive-mind-router` sidecar 转发,使每个任务获得仅限自身的令牌和自己的请求日志——参见[路由器隔离](./docs/ROUTER.zh.md)(实验性)与[收集日志](./docs/COLLECTING-LOGS.zh.md)。
92
+
91
93
  运行 `hive.mjs` 的最低系统要求:
92
94
 
93
95
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.13.5",
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.