config: `config_set` writes under a top-level `values` envelope that the daemon's config.json reader never unwraps, so `config_set daemon.idleSecs 0` cannot reach the daemon
- Dominant language
- TypeScript
- Stars
- 72.6k
- Forks
- 8.6k
- Avg merge
- 3d 3h
- Merged PRs (30d)
- 85
Description
# config: `config_set` writes under a top-level `values` envelope that the daemon's config.json reader never unwraps, so `config_set daemon.idleSecs 0` cannot reach the daemon
**Environment**
- ruflo 3.38.19
- node v24.6.0
- macOS 26.6.2
- agentdb 3.0.0-alpha.20
**Steps to reproduce**
1. `config_set` (MCP tool) `daemon.idleSecs` to `0` with default scope.
2. Inspect the resulting `.claude-flow/config.json`.
3. Start the daemon and check whether the idle-shutdown limit changed.
**Observed**
`config_set` writes:
```json
{
"values": { "daemon.idleSecs": 0 },
"scopes": {},
"version": "3.0.0",
"updatedAt": "..."
}
```
The daemon's own config loader reads the *top level* of the same file for the flat key, or `scopes.project`, never `values`:
```js
const cfg = raw?.scopes?.project ?? raw;
const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
```
`raw['daemon.idleSecs']` is `undefined` (the real value lives at `raw.values['daemon.idleSecs']`), and `raw.scopes.project` is also empty (the write went to `raw.values`, not `raw.scopes.project`, because `config_set` was called with default scope). So the write silently has no effect on the daemon.
**Expected**
A value written through the config door (`config_set`) that shares a key name with what the daemon loader reads (`daemon.idleSecs`) should reach the daemon — canon's own two components read and write the same physical file but disagree on the file's shape.
**Root cause — corrected from the initial estate hypothesis**
The estate's working theory going in was that `config_set` calls a `setNestedValue()` helper that expands a dotted key into a nested object (`{"daemon":{"idleSecs":0}}`), and that *this* nesting was the mismatch. That is not what happens: `setNestedValue()` is defined in `config-tools.ts`/`.js` but is **not called anywhere** in that file — it is dead code. The real handler for `config_set` with default scope does a flat assignment with the dotted key left intact as one string, and the mismatch is the `values` **envelope**, not object nesting.
Upstream at pinned sha `db4991967c45c6f72133dff0bb80b0a492960fc1`, `v3/@claude-flow/cli/src/mcp-tools/config-tools.ts`:
```
101: function setNestedValue(obj: Record, key: string, value: unknown): void {
```
(only definition of `setNestedValue` in the file — confirmed via `grep -n "setNestedValue("`, one hit).
```
194: const store = loadConfigStore();
195: const key = input.key as string;
196: const value = input.value;
197: const scope = (input.scope as string) || 'default';
198:
199: const previousValue = store.values[key];
200:
201: if (scope === 'default') {
202: store.values[key] = value;
203: } else {
204: if (!store.scopes[scope]) {
205: store.scopes[scope] = {};
206: }
207: store.scopes[scope][key] = value;
208: }
```
(shown as :198-207 above; installed dist has the identical assignment at `@claude-flow/cli/dist/src/mcp-tools/config-tools.js` :174-181 — `const previousValue = store.values[key];` at :178, `store.values[key] = value;` at :180 — and defines `setNestedValue` unused at :84, matching the upstream shape.)
The daemon's flat-key reader, upstream `v3/@claude-flow/cli/src/services/worker-daemon.ts` (comment references `#2356`):
```
546: // #2356 — lifecycle limits are configured in SECONDS in config.json
547: // (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
549: const rawTtl = cfg['daemon.ttlSecs'] ?? raw['daemon.ttlSecs'];
550: const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
```
Installed dist, `@claude-flow/cli/dist/src/services/worker-daemon.js`:
```
396: // #2356 — lifecycle limits are configured in SECONDS in config.json
397: // (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
399: const rawTtl = cfg['daemon.ttlSecs'] ?? raw['daemon.ttlSecs'];
400: const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
```
Both `cfg` (`raw?.scopes?.project ?? raw`) and the bare `raw` fallback look at the file's top level or `scopes.project` — neither ever looks under `values`, which is where `config_set`'s default-scope path actually puts the value.
**Suggested fix (sketch)**
Either side closes the gap; smallest change is on the daemon reader, since `values` is `config_set`'s well-established default-scope location and other config consumers may already expect it there:
```diff
- const cfg = raw?.scopes?.project ?? raw;
+ const cfg = raw?.scopes?.project ?? raw?.values ?? raw;
const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
```
Alternatively, `config_set` could write default-scope values at the file's top level instead of under `values`, but that risks colliding with the `scopes`/`version`/`updatedAt` keys the store file already uses at that level, so the reader-side fix above is the lower-risk one.
**Related**
- #2356 (introduces the flat `daemon.ttlSecs`/`daemon.idleSecs` keys the daemon reads)
- Companion issue `DAEMON-idle-check-restored-lastRun.md` — `daemon.idleSecs: 0` is the workaround for that bug, and this defect is why the workaround cannot currently be applied through the config door at all (a hand-edited `config.json` with the key at top level does work, since it bypasses `config_set`'s envelope).
**Evidence / estate provenance**
`task/task-1788565562107-hrvg6b/receipts` (ns `final`), SCOPE NOTE paragraph — this is where the estate discovered the mismatch operationally (had to hand-edit `config.json` with a flat top-level key rather than use `config_set`); cross-referenced against `task/task-1788563164755-ticrih/recon-c5-sona-hnsw` D-C. The `setNestedValue`-is-dead-code correction above was verified directly against both the pinned-sha upstream source and the installed dist for this filing, not carried over unverified from the estate's package brief.
Contributor guide
Research direction
Read v3/@claude-flow/cli/src/mcp-tools/config-tools.ts and v3/@claude-flow/cli/src/services/worker-daemon.ts, then reproduce the default-scope config_set flow described here. Compare the source with the installed dist counterparts and verify that a default-scope daemon.idleSecs value is consumed by the daemon without breaking scoped configuration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- backend, cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100