Skip to content

Commit e733bda

Browse files
authored
fix(daemon): report the running daemon's TTL in daemon status (#4238) (#4241)
daemon status printed config.ttlMs of the default-config WorkerDaemon it builds in the status process, so a background daemon started with --ttl 0 showed 'TTL: 12h (self-shutdown)'. Read ttlMs from daemon-state.json when the background daemon is running, as is already done for aiWorkersEnabled and for daemon status --all.
1 parent ca31fc5 commit e733bda

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* #4238: `daemon status` printed the TTL of the status process's own
3+
* default-config WorkerDaemon (12h), not the running background daemon's,
4+
* so a daemon started with `--ttl 0` was reported as self-shutting down.
5+
* The background daemon persists its real config in daemon-state.json.
6+
*/
7+
import { afterEach, describe, expect, it, vi } from 'vitest';
8+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
9+
import { join } from 'node:path';
10+
import { tmpdir } from 'node:os';
11+
import { daemonCommand } from '../src/commands/daemon.js';
12+
import { output } from '../src/output.js';
13+
import type { CommandContext } from '../src/types.js';
14+
15+
const originalCwd = process.cwd();
16+
let workspace: string | undefined;
17+
18+
afterEach(() => {
19+
process.chdir(originalCwd);
20+
vi.restoreAllMocks();
21+
if (workspace) rmSync(workspace, { recursive: true, force: true });
22+
workspace = undefined;
23+
});
24+
25+
async function statusBoxFor(config: Record<string, unknown>): Promise<string> {
26+
workspace = mkdtempSync(join(tmpdir(), 'daemon-status-ttl-'));
27+
const stateDir = join(workspace, '.claude-flow');
28+
mkdirSync(stateDir, { recursive: true });
29+
// A live PID stands in for the background daemon.
30+
writeFileSync(join(stateDir, 'daemon.pid'), String(process.pid));
31+
writeFileSync(join(stateDir, 'daemon-state.json'), JSON.stringify({ running: true, config }));
32+
process.chdir(workspace);
33+
const lines: string[] = [];
34+
vi.spyOn(output, 'printBox').mockImplementation((content: string | string[]) => {
35+
lines.push(...(Array.isArray(content) ? content : [content]));
36+
});
37+
vi.spyOn(output, 'writeln').mockImplementation(() => undefined);
38+
const status = daemonCommand.subcommands!.find(c => c.name === 'status')!;
39+
await status.action!({ args: [], flags: {}, cwd: workspace, interactive: false } as unknown as CommandContext);
40+
return lines.join('\n');
41+
}
42+
43+
describe('daemon status TTL (#4238)', () => {
44+
it('reports TTL off when the running background daemon has ttlMs 0', async () => {
45+
const box = await statusBoxFor({ ttlMs: 0 });
46+
expect(box).toContain('off (runs until stopped)');
47+
expect(box).not.toContain('(self-shutdown)');
48+
});
49+
50+
it('reports the background daemon TTL rather than the 12h default', async () => {
51+
expect(await statusBoxFor({ ttlMs: 2 * 3600000 })).toContain('TTL: 2h (self-shutdown)');
52+
});
53+
});

‎v3/@claude-flow/cli/src/commands/daemon.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,12 +1294,18 @@ const statusCommand: Command = {
12941294
// its real consent state into daemon-state.json — prefer that when it
12951295
// is the one running.
12961296
let aiWorkersEnabled = status.config.aiWorkersEnabled;
1297+
// #4238: same for the TTL — a daemon started with `--ttl 0` must not be
1298+
// reported with this process's 12h default.
1299+
let ttlMs = status.config.ttlMs;
12971300
if (bgRunning) {
12981301
try {
12991302
const st = JSON.parse(fs.readFileSync(join(projectRoot, '.claude-flow', 'daemon-state.json'), 'utf-8'));
13001303
if (typeof st?.config?.aiWorkersEnabled === 'boolean') {
13011304
aiWorkersEnabled = st.config.aiWorkersEnabled;
13021305
}
1306+
if (typeof st?.config?.ttlMs === 'number' && Number.isFinite(st.config.ttlMs)) {
1307+
ttlMs = st.config.ttlMs;
1308+
}
13031309
} catch { /* no/partial state — fall back to in-process config */ }
13041310
}
13051311

@@ -1315,8 +1321,8 @@ const statusCommand: Command = {
13151321
`Status: ${statusIcon} ${statusText}${mode}`,
13161322
`PID: ${displayPid}`,
13171323
status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
1318-
status.config.ttlMs > 0
1319-
? `TTL: ${Math.round(status.config.ttlMs / 3600000)}h (self-shutdown)`
1324+
ttlMs > 0
1325+
? `TTL: ${Math.round(ttlMs / 3600000)}h (self-shutdown)`
13201326
: `TTL: ${output.dim('off (runs until stopped)')}`,
13211327
// #2661: surface the AI-consent gate so "why is audit local-only?"
13221328
// is answerable from `daemon status` alone.

0 commit comments

Comments
 (0)