claude-multiacc 2.0.31 → 2.0.32

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/docs/CODEX.md CHANGED
@@ -88,16 +88,19 @@ The addon does not choose delegation policy by default. To manage the concurrent
88
88
  subagent ceiling across one Codex pool, explicitly configure it (Python 3.11+):
89
89
 
90
90
  ```bash
91
- codex-accounts configure --max-subagents 8 --include-global --json
92
- codex-accounts configure --max-subagents 8 --include-global --check --json
91
+ codex-accounts configure --max-subagents 7 --include-global --json
92
+ codex-accounts configure --max-subagents 7 --include-global --check --json
93
93
  ```
94
94
 
95
95
  An explicitly selected modern Python is preserved. If the shell resolves an older
96
96
  system Python, this command tries already installed Python 3.11+ executables on
97
97
  PATH and in standard Homebrew locations, with bounded probes and no downloads.
98
98
 
99
- The first command sets `agents.max_concurrent_threads_per_session` in every manifest
99
+ The first command sets `features.multi_agent_v2.max_concurrent_threads_per_session`
100
+ and the compatible `agents.max_concurrent_threads_per_session` in every manifest
100
101
  account's `config.toml`, migrating its legacy `agents.max_threads` key when present.
102
+ An existing boolean `multi_agent_v2` becomes a table with the same `enabled` value;
103
+ an existing feature cap is updated along with the agents cap.
101
104
  `--include-global` also updates the machine's `~/.codex/config.toml`; without it,
102
105
  the global file is untouched. Unrelated models, instructions, MCP servers, project
103
106
  trust and custom agents remain intact. Retired directories outside the manifest
@@ -112,16 +115,16 @@ fails sync instead of reporting success. Remote sync does not change the target'
112
115
  global config. Without the policy file, existing settings keep their usual
113
116
  copy-only-if-missing behavior and no new remote command runs.
114
117
 
115
- `--check` never writes; it checks the requested value against the persisted policy
116
- and selected configs, returning exit 1 on drift. Without `--max-subagents`, the
117
- command only reads the current pool policy and configuration. JSON reports contain
118
+ `--check` never writes; it checks both caps against the persisted policy
119
+ and requested value in selected configs, returning exit 1 on drift. Without
120
+ `--max-subagents`, the command only reads the current pool policy and configuration. JSON reports contain
118
121
  the installed `plugin_version`, `max_subagents`, `policy_max_subagents`,
119
122
  `accounts_total`, `accounts_configured`, `global_configured`, `mismatch_count` and
120
123
  `verified`, without account identities, paths, credentials or arbitrary config.
121
124
 
122
- Conflicting profile or `features.multi_agent_v2` object overrides are rejected.
125
+ Conflicting profile overrides are rejected.
123
126
  Unusual inline-table layouts are also rejected before configuration changes;
124
- use an ordinary `[agents]` table for the managed setting. All target configs are
125
- validated before writes, each replacement is atomic, and repeating the command
127
+ use ordinary `[agents]` and `[features.multi_agent_v2]` tables for the managed settings.
128
+ All target configs are validated before writes, each replacement is atomic, and repeating the command
126
129
  repairs an interrupted write. Existing running sessions retain the settings they
127
130
  loaded; the policy applies to subsequent sessions.
@@ -8,7 +8,6 @@ except ImportError:
8
8
  raise SystemExit("Codex settings configuration requires Python 3.11 or newer") from None
9
9
 
10
10
  KEY = "max_concurrent_threads_per_session"
11
- ALIASES = (KEY, "max_threads")
12
11
 
13
12
 
14
13
  def limits(document):
@@ -33,39 +32,61 @@ def validate_overrides(document, value):
33
32
  raise ValueError(f"{name}: profile overrides the requested subagent limit")
34
33
 
35
34
 
35
+ def dotted_pattern(parts):
36
+ return r'\s*\.\s*'.join(r'(?:' + re.escape(part) + r'|"' + re.escape(part)
37
+ + r'"|\x27' + re.escape(part) + r'\x27)' for part in parts)
38
+
39
+
40
+ def edit_setting(source, table, key, literal, aliases=()):
41
+ """Edit table/dotted assignments, or remove one when literal is None."""
42
+ headers = [re.compile(r'^\s*\[\s*' + dotted_pattern(table[:depth]) + r'\s*\]\s*(?:#.*)?$')
43
+ for depth in range(1, len(table) + 1)]
44
+ assignments = [re.compile(r'^\s*(?:' + '|'.join(dotted_pattern((*table[depth:], name))
45
+ for name in (key, *aliases)) + r')\s*=')
46
+ for depth in range(len(table) + 1)]
47
+ section, output, inserted = 0, [], False
48
+ for line in source.splitlines(keepends=True):
49
+ if line.lstrip().startswith("["):
50
+ section = next((depth for depth, header in enumerate(headers, 1)
51
+ if header.match(line.rstrip("\r\n"))), None)
52
+ if section == len(table) and literal is not None and not inserted:
53
+ output.extend([line.rstrip("\r\n") + "\n", f"{key} = {literal}\n"])
54
+ inserted = True
55
+ continue
56
+ if section is not None and assignments[section].match(line):
57
+ if literal is not None and not inserted:
58
+ output.append(f"{'.'.join((*table[section:], key))} = {literal}\n")
59
+ inserted = True
60
+ continue
61
+ output.append(line)
62
+ if literal is not None and not inserted:
63
+ output.extend([f"\n[{'.'.join(table)}]\n", f"{key} = {literal}\n"])
64
+ return "".join(output)
65
+
66
+
36
67
  def updated_config(source, value):
37
- """Edit ordinary [agents] or dotted keys, then prove no unrelated TOML value changed."""
68
+ """Set the v2 feature cap and agents fallback, preserving enablement and unrelated values."""
38
69
  original = tomllib.loads(source)
39
- validate_overrides(original, value)
70
+ validate_overrides({"profiles": original.get("profiles", {})}, value)
40
71
  expected = copy.deepcopy(original)
41
72
  agents = expected.setdefault("agents", {})
42
73
  agents.pop("max_threads", None)
43
74
  agents[KEY] = value
44
- lines, section, output, inserted = source.splitlines(keepends=True), "", [], False
45
- header = re.compile(r'^\s*\[\s*(?:agents|"agents"|\x27agents\x27)\s*\]\s*(?:#.*)?$')
46
- assignment = re.compile(r'^\s*(?:"|\x27)?(?:' + '|'.join(ALIASES) + r')(?:"|\x27)?\s*=')
47
- dotted = re.compile(r'^\s*agents\s*\.\s*(?:' + '|'.join(ALIASES) + r')\s*=')
48
- for line in lines:
49
- if header.match(line.rstrip("\r\n")):
50
- section = "agents"
51
- output.extend([line.rstrip("\r\n") + "\n", f"{KEY} = {value}\n"])
52
- inserted = True
53
- continue
54
- if line.lstrip().startswith("["):
55
- section = "other"
56
- if (section == "agents" and assignment.match(line)) or (not section and dotted.match(line)):
57
- continue
58
- output.append(line)
59
- if not inserted:
60
- if any(dotted.match(line) for line in lines):
61
- output.insert(0, f"agents.{KEY} = {value}\n")
62
- else:
63
- output.extend(["\n[agents]\n", f"{KEY} = {value}\n"])
64
- result = "".join(output)
75
+ features = expected.setdefault("features", {})
76
+ feature = features.get("multi_agent_v2")
77
+ result = edit_setting(source, ("agents",), KEY, value, ("max_threads",))
78
+ if isinstance(feature, bool):
79
+ result = edit_setting(result, ("features",), "multi_agent_v2", None)
80
+ result = edit_setting(result, ("features", "multi_agent_v2"), "enabled", str(feature).lower())
81
+ features["multi_agent_v2"] = {"enabled": feature}
82
+ elif feature is None:
83
+ features["multi_agent_v2"] = {}
84
+ features["multi_agent_v2"][KEY] = value
85
+ result = edit_setting(result, ("features", "multi_agent_v2"), KEY, value)
65
86
  try:
66
87
  unchanged = tomllib.loads(result) == expected
67
88
  except tomllib.TOMLDecodeError:
68
89
  unchanged = False
69
90
  if not unchanged:
70
- raise ValueError("unsupported agents TOML layout; use a normal [agents] table before configuring")
91
+ raise ValueError("unsupported Codex TOML layout; use normal [agents] and [features.multi_agent_v2] tables")
71
92
  return result
@@ -92,7 +92,9 @@ def readback(paths, value):
92
92
  result = {}
93
93
  for path in paths:
94
94
  document = tomllib.loads(path.read_text()) if path.exists() else {}
95
- matches = limits(document)["effective"] == value if value is not None else True
95
+ current = limits(document)
96
+ matches = (all(current[key] == value for key in ("configured", "feature_override"))
97
+ if value is not None else True)
96
98
  try:
97
99
  validate_overrides(document, value)
98
100
  except ValueError:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.31",
3
+ "version": "2.0.32",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,14 +33,30 @@ class CodexConfigEditTests(unittest.TestCase):
33
33
  self.assertEqual(tomllib.loads(updated)["agents"][KEY], 8)
34
34
  self.assertEqual(updated_config(updated, 8), updated)
35
35
 
36
- def test_conflicting_feature_and_profile_overrides_are_rejected(self):
37
- for source in ('[features.multi_agent_v2]\nmax_concurrent_threads_per_session = 20\n',
36
+ def test_conflicting_profile_overrides_are_rejected(self):
37
+ for source in ('[profiles.quick.features.multi_agent_v2]\nmax_concurrent_threads_per_session = 20\n',
38
38
  '[profiles.quick.agents]\nmax_threads = 20\n'):
39
39
  with self.subTest(source=source), self.assertRaisesRegex(ValueError, "overrides"):
40
40
  updated_config(source, 8)
41
41
  source = '[features.multi_agent_v2]\nmax_concurrent_threads_per_session = 8\n'
42
42
  self.assertEqual(tomllib.loads(updated_config(source, 8))["agents"][KEY], 8)
43
43
 
44
+ def test_existing_eight_thread_policies_and_feature_overrides_change_to_seven(self):
45
+ for feature in ('[features]\nmulti_agent_v2 = true\n',
46
+ '[features]\nmulti_agent_v2 = false\n',
47
+ '[features.multi_agent_v2]\nenabled = true\nmax_concurrent_threads_per_session = 20\n',
48
+ 'features.multi_agent_v2.max_concurrent_threads_per_session = 8\n',
49
+ '[features]\nmulti_agent_v2.max_concurrent_threads_per_session = 8\n'):
50
+ source = feature + '[agents]\nmax_concurrent_threads_per_session = 8\n'
51
+ with self.subTest(feature=feature):
52
+ updated = updated_config(source, 7)
53
+ document = tomllib.loads(updated)
54
+ self.assertEqual(document["agents"][KEY], 7)
55
+ self.assertEqual(document["features"]["multi_agent_v2"][KEY], 7)
56
+ if 'false' in feature:
57
+ self.assertFalse(document["features"]["multi_agent_v2"]["enabled"])
58
+ self.assertEqual(updated_config(updated, 7), updated)
59
+
44
60
  def test_unsupported_layouts_and_string_lookalikes_fail_safely(self):
45
61
  for source in ('agents = { max_threads = 4 }\n',
46
62
  'instructions = """\n[agents]\nmax_threads = 99\n"""\n'):
@@ -79,13 +79,31 @@ class CodexSettingsTests(unittest.TestCase):
79
79
 
80
80
  def test_invalid_config_is_rejected_before_any_configuration_is_changed(self):
81
81
  before = self.account.read_text()
82
- self.global_config.write_text('[features.multi_agent_v2]\nmax_concurrent_threads_per_session = 20\n')
82
+ self.global_config.write_text('[profiles.quick.features.multi_agent_v2]\n'
83
+ 'max_concurrent_threads_per_session = 20\n')
83
84
  result = self.command("configure", "--max-subagents", "8", "--include-global", "--json")
84
85
  self.assertNotEqual(result.returncode, 0)
85
86
  self.assertIn("overrides", result.stderr)
86
87
  self.assertEqual(self.account.read_text(), before)
87
88
  self.assertFalse((self.pool / "codex-settings-policy.json").exists())
88
89
 
90
+ def test_seven_thread_policy_requires_v2_setting_and_repairs_legacy_only_configs(self):
91
+ result = self.command("configure", "--max-subagents", "8", "--include-global", "--json")
92
+ self.assertEqual(result.returncode, 0, result.stderr)
93
+ result = self.command("configure", "--max-subagents", "7", "--include-global", "--json")
94
+ self.assertEqual(result.returncode, 0, result.stderr)
95
+ for path in (self.account, self.global_config):
96
+ config = tomllib.loads(path.read_text())
97
+ self.assertEqual(config["features"]["multi_agent_v2"][KEY], 7)
98
+ self.assertEqual(config["agents"][KEY], 7)
99
+ self.account.write_text('[agents]\nmax_concurrent_threads_per_session = 7\n')
100
+ result = self.command("configure", "--include-global", "--check", "--json")
101
+ self.assertEqual(result.returncode, 1, result.stderr)
102
+ self.assertEqual(json.loads(result.stdout)["mismatch_count"], 1)
103
+ result = self.command("configure", "--max-subagents", "7", "--include-global", "--json")
104
+ self.assertEqual(result.returncode, 0, result.stderr)
105
+ self.assertTrue(json.loads(result.stdout)["verified"])
106
+
89
107
  def test_symlinked_global_config_remains_linked(self):
90
108
  self.account.unlink()
91
109
  self.account.symlink_to(self.global_config)