From 5ca0633c270e182ba4ffe76903f3143d379ca541 Mon Sep 17 00:00:00 2001 From: Michael Bravo Date: Tue, 10 Mar 2026 15:58:24 +0200 Subject: [PATCH 1/9] fix: refresh tasks snapshot immediately after IPC task mutations Previously, current_tasks.json was only written at container-start time, so tasks created (or paused/cancelled/updated) during a session were invisible to list_tasks until the next invocation. Add an onTasksChanged callback to IpcDeps, called after every successful mutation in processTaskIpc (schedule_task, pause_task, resume_task, cancel_task, update_task). index.ts wires it up to write fresh snapshots for all registered groups immediately, keeping no new coupling between ipc.ts and the container layer. Co-Authored-By: Claude Sonnet 4.6 --- src/index.ts | 15 +++++++++++++++ src/ipc-auth.test.ts | 1 + src/ipc.ts | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/src/index.ts b/src/index.ts index c6295c5..bfdcdec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -575,6 +575,21 @@ async function main(): Promise { getAvailableGroups, writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj), + onTasksChanged: () => { + const tasks = getAllTasks(); + const taskRows = tasks.map((t) => ({ + id: t.id, + groupFolder: t.group_folder, + prompt: t.prompt, + schedule_type: t.schedule_type, + schedule_value: t.schedule_value, + status: t.status, + next_run: t.next_run, + })); + for (const group of Object.values(registeredGroups)) { + writeTasksSnapshot(group.folder, group.isMain === true, taskRows); + } + }, }); queue.setProcessMessagesFn(processGroupMessages); recoverPendingMessages(); diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts index 1aa681e..0adf899 100644 --- a/src/ipc-auth.test.ts +++ b/src/ipc-auth.test.ts @@ -62,6 +62,7 @@ beforeEach(() => { syncGroups: async () => {}, getAvailableGroups: () => [], writeGroupsSnapshot: () => {}, + onTasksChanged: () => {}, }; }); diff --git a/src/ipc.ts b/src/ipc.ts index 7a972c0..48efeb5 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -22,6 +22,7 @@ export interface IpcDeps { availableGroups: AvailableGroup[], registeredJids: Set, ) => void; + onTasksChanged: () => void; } let ipcWatcherRunning = false; @@ -270,6 +271,7 @@ export async function processTaskIpc( { taskId, sourceGroup, targetFolder, contextMode }, 'Task created via IPC', ); + deps.onTasksChanged(); } break; @@ -282,6 +284,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task paused via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -300,6 +303,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task resumed via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -318,6 +322,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task cancelled via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -388,6 +393,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup, updates }, 'Task updated via IPC', ); + deps.onTasksChanged(); } break; From d49af91cc24114502bfa7907a4d7b448f3b0da2e Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Mon, 16 Mar 2026 09:28:13 +0000 Subject: [PATCH 2/9] fix: auto-accept remote-control prompt to prevent immediate exit `claude remote-control` prompts "Enable Remote Control? (y/n)" on every launch. With stdin set to 'ignore', the process exits immediately because it cannot read the response. Pipe 'y\n' to stdin instead. Co-Authored-By: Claude Opus 4.6 --- src/remote-control.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/remote-control.ts b/src/remote-control.ts index df8f646..2f0bdc4 100644 --- a/src/remote-control.ts +++ b/src/remote-control.ts @@ -111,7 +111,7 @@ export async function startRemoteControl( try { proc = spawn('claude', ['remote-control', '--name', 'NanoClaw Remote'], { cwd, - stdio: ['ignore', stdoutFd, stderrFd], + stdio: ['pipe', stdoutFd, stderrFd], detached: true, }); } catch (err: any) { @@ -120,6 +120,12 @@ export async function startRemoteControl( return { ok: false, error: `Failed to start: ${err.message}` }; } + // Auto-accept the "Enable Remote Control?" prompt + if (proc.stdin) { + proc.stdin.write('y\n'); + proc.stdin.end(); + } + // Close FDs in the parent — the child inherited copies fs.closeSync(stdoutFd); fs.closeSync(stderrFd); @@ -196,9 +202,11 @@ export async function startRemoteControl( }); } -export function stopRemoteControl(): { - ok: true; -} | { ok: false; error: string } { +export function stopRemoteControl(): + | { + ok: true; + } + | { ok: false; error: string } { if (!activeSession) { return { ok: false, error: 'No active Remote Control session' }; } From 924482870e5b996a02ed46576b6d243c349a85d0 Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Mon, 16 Mar 2026 09:41:09 +0000 Subject: [PATCH 3/9] test: update remote-control tests for stdin pipe change Co-Authored-By: Claude Opus 4.6 --- src/remote-control.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/remote-control.test.ts b/src/remote-control.test.ts index 4b5ab2f..354069e 100644 --- a/src/remote-control.test.ts +++ b/src/remote-control.test.ts @@ -24,7 +24,12 @@ import { // --- Helpers --- function createMockProcess(pid = 12345) { - return { pid, unref: vi.fn(), kill: vi.fn() }; + return { + pid, + unref: vi.fn(), + kill: vi.fn(), + stdin: { write: vi.fn(), end: vi.fn() }, + }; } describe('remote-control', () => { @@ -101,8 +106,8 @@ describe('remote-control', () => { const spawnCall = spawnMock.mock.calls[0]; const options = spawnCall[2]; - // stdio should use file descriptors (numbers), not 'pipe' - expect(options.stdio[0]).toBe('ignore'); + // stdio[0] is 'pipe' so we can write 'y' to accept the prompt + expect(options.stdio[0]).toBe('pipe'); expect(typeof options.stdio[1]).toBe('number'); expect(typeof options.stdio[2]).toBe('number'); }); From 12ff2589facf2e53421b4336c07c509744de0257 Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Mon, 16 Mar 2026 11:51:47 +0200 Subject: [PATCH 4/9] style: format remote-control tests with prettier Co-Authored-By: Claude Opus 4.6 (1M context) --- src/remote-control.test.ts | 43 +++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/remote-control.test.ts b/src/remote-control.test.ts index 354069e..24e1b11 100644 --- a/src/remote-control.test.ts +++ b/src/remote-control.test.ts @@ -50,14 +50,20 @@ describe('remote-control', () => { stdoutFileContent = ''; // Default fs mocks - mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined as any); - writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + mkdirSyncSpy = vi + .spyOn(fs, 'mkdirSync') + .mockImplementation(() => undefined as any); + writeFileSyncSpy = vi + .spyOn(fs, 'writeFileSync') + .mockImplementation(() => {}); unlinkSyncSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {}); openSyncSpy = vi.spyOn(fs, 'openSync').mockReturnValue(42 as any); closeSyncSpy = vi.spyOn(fs, 'closeSync').mockImplementation(() => {}); // readFileSync: return stdoutFileContent for the stdout file, state file, etc. - readFileSyncSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(((p: string) => { + readFileSyncSpy = vi.spyOn(fs, 'readFileSync').mockImplementation((( + p: string, + ) => { if (p.endsWith('remote-control.stdout')) return stdoutFileContent; if (p.endsWith('remote-control.json')) { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); @@ -79,7 +85,8 @@ describe('remote-control', () => { spawnMock.mockReturnValue(proc); // Simulate URL appearing in stdout file on first poll - stdoutFileContent = 'Session URL: https://claude.ai/code?bridge=env_abc123\n'; + stdoutFileContent = + 'Session URL: https://claude.ai/code?bridge=env_abc123\n'; vi.spyOn(process, 'kill').mockImplementation((() => true) as any); const result = await startRemoteControl('user1', 'tg:123', '/project'); @@ -162,7 +169,9 @@ describe('remote-control', () => { spawnMock.mockReturnValueOnce(proc1).mockReturnValueOnce(proc2); // First start: process alive, URL found - const killSpy = vi.spyOn(process, 'kill').mockImplementation((() => true) as any); + const killSpy = vi + .spyOn(process, 'kill') + .mockImplementation((() => true) as any); stdoutFileContent = 'https://claude.ai/code?bridge=env_first\n'; await startRemoteControl('user1', 'tg:123', '/project'); @@ -244,7 +253,9 @@ describe('remote-control', () => { const proc = createMockProcess(55555); spawnMock.mockReturnValue(proc); stdoutFileContent = 'https://claude.ai/code?bridge=env_stop\n'; - const killSpy = vi.spyOn(process, 'kill').mockImplementation((() => true) as any); + const killSpy = vi + .spyOn(process, 'kill') + .mockImplementation((() => true) as any); await startRemoteControl('user1', 'tg:123', '/project'); @@ -342,7 +353,9 @@ describe('remote-control', () => { if (p.endsWith('remote-control.json')) return JSON.stringify(session); return ''; }) as any); - const killSpy = vi.spyOn(process, 'kill').mockImplementation((() => true) as any); + const killSpy = vi + .spyOn(process, 'kill') + .mockImplementation((() => true) as any); restoreRemoteControl(); expect(getActiveSession()).not.toBeNull(); @@ -370,13 +383,15 @@ describe('remote-control', () => { restoreRemoteControl(); - return startRemoteControl('user2', 'tg:456', '/project').then((result) => { - expect(result).toEqual({ - ok: true, - url: 'https://claude.ai/code?bridge=env_restored', - }); - expect(spawnMock).not.toHaveBeenCalled(); - }); + return startRemoteControl('user2', 'tg:456', '/project').then( + (result) => { + expect(result).toEqual({ + ok: true, + url: 'https://claude.ai/code?bridge=env_restored', + }); + expect(spawnMock).not.toHaveBeenCalled(); + }, + ); }); }); }); From 260812702c876e88db9a26c3792e096ae5e2e396 Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Mon, 16 Mar 2026 13:12:07 +0200 Subject: [PATCH 5/9] fix: add KillMode=process so remote-control survives restarts systemd's default KillMode=control-group kills all processes in the cgroup on service restart, including the detached claude remote-control process. KillMode=process only kills the main Node.js process, letting detached children survive. restoreRemoteControl() already handles reattaching on startup. Co-Authored-By: Claude Opus 4.6 (1M context) --- setup/service.test.ts | 11 +++++++++++ setup/service.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/setup/service.test.ts b/setup/service.test.ts index eb15db8..9168fe1 100644 --- a/setup/service.test.ts +++ b/setup/service.test.ts @@ -62,6 +62,7 @@ ExecStart=${nodePath} ${projectRoot}/dist/index.js WorkingDirectory=${projectRoot} Restart=always RestartSec=5 +KillMode=process Environment=HOME=${homeDir} Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin StandardOutput=append:${projectRoot}/logs/nanoclaw.log @@ -142,6 +143,16 @@ describe('systemd unit generation', () => { expect(unit).toContain('RestartSec=5'); }); + it('uses KillMode=process to preserve detached children', () => { + const unit = generateSystemdUnit( + '/usr/bin/node', + '/home/user/nanoclaw', + '/home/user', + false, + ); + expect(unit).toContain('KillMode=process'); + }); + it('sets correct ExecStart', () => { const unit = generateSystemdUnit( '/usr/bin/node', diff --git a/setup/service.ts b/setup/service.ts index 643c8c9..71b3c63 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -243,6 +243,7 @@ ExecStart=${nodePath} ${projectRoot}/dist/index.js WorkingDirectory=${projectRoot} Restart=always RestartSec=5 +KillMode=process Environment=HOME=${homeDir} Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin StandardOutput=append:${projectRoot}/logs/nanoclaw.log From 8b647410c6e17fa4ff4c2801ce441850f283fb79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Mar 2026 17:37:14 +0000 Subject: [PATCH 6/9] chore: bump version to 1.2.15 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04b06a2..ee97d7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.14", + "version": "1.2.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 76d9bfa..97c3a6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.14", + "version": "1.2.15", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From c8f03eddeb1f69b514e5ca13f2cad38dd329c5f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Mar 2026 17:37:19 +0000 Subject: [PATCH 7/9] =?UTF-8?q?docs:=20update=20token=20count=20to=2040.5k?= =?UTF-8?q?=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index 4fd94b8..480cd9f 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.4k tokens, 20% of context window + + 40.5k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.4k + + 40.5k From 9200612dd1619aed22d4a1608230dd576cf0eba0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Mar 2026 09:52:20 +0000 Subject: [PATCH 8/9] chore: bump version to 1.2.16 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee97d7c..ad5f762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.15", + "version": "1.2.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.15", + "version": "1.2.16", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 97c3a6f..44312f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.15", + "version": "1.2.16", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From e7d0ffb208aef464ec5b30e9e3bfc0c7c87f4bb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Mar 2026 09:52:28 +0000 Subject: [PATCH 9/9] =?UTF-8?q?docs:=20update=20token=20count=20to=2040.6k?= =?UTF-8?q?=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index 480cd9f..ce35723 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.5k tokens, 20% of context window + + 40.6k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.5k + + 40.6k