From 66b0aded2838f1259d18d668b2964df0f31cde8e Mon Sep 17 00:00:00 2001 From: Saurabh Sungh Date: Thu, 5 Nov 2026 05:09:54 +0530 Subject: [PATCH] fix(cli): add KillMode=mixed to generated systemd unit (#18598) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The CLI subsystem provides background service management (`paperclipai service install`) generating systemd user units on Linux. > - When systemctl stops or restarts the service, systemd uses default `KillMode=control-group` and sends SIGTERM simultaneously to all cgroup processes, including embedded PostgreSQL. > - This races the server's clean shutdown sequence and triggers a smart shutdown in PostgreSQL instead of a fast shutdown, leading to ungraceful exit. > - This pull request adds `KillMode=mixed` to the `[Service]` section of the generated systemd unit definition. > - The benefit is that SIGTERM is delivered only to the main server process, allowing the server to cleanly drain and shut down embedded PostgreSQL in order. ## Linked Issues or Issue Description Fixes #18595 ## What Changed - Added `KillMode=mixed` to `renderSystemdUnit` under `[Service]` in `cli/src/services/service-manager.ts`. - Added test assertion for `KillMode=mixed` in `cli/src/__tests__/service-manager.test.ts`. ## Verification - Automated test: `cli/src/__tests__/service-manager.test.ts` checks that `renderSystemdUnit` generates a unit containing `KillMode=mixed`. - Manual verification: Checked rendered systemd unit output to ensure valid systemd syntax and correct section placement under `[Service]`. ## Risks - Low risk: `KillMode=mixed` only applies to systemd user services on Linux generated via `paperclipai service install`. If the main process fails to exit within `TimeoutStopSec` (300s), systemd sends SIGKILL to all remaining child processes. ## Model Used - Google Antigravity (Gemini 3.8 Flash High) with chain-of-thought reasoning and codebase tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green --- cli/src/__tests__/service-manager.test.ts | 1 + cli/src/services/service-manager.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/cli/src/__tests__/service-manager.test.ts b/cli/src/__tests__/service-manager.test.ts index f64a3125521..c075a178f46 100644 --- a/cli/src/__tests__/service-manager.test.ts +++ b/cli/src/__tests__/service-manager.test.ts @@ -31,6 +31,7 @@ describe("service definition generation", () => { const unit = renderSystemdUnit({ instanceId: "team-a", shimPath: "/home/alice/.local/bin/paperclipai", homeDir: "/home/alice/.paperclip" }); expect(unit).toContain("Type=notify"); expect(unit).toContain("NotifyAccess=all"); + expect(unit).toContain("KillMode=mixed"); expect(unit).toContain('ExecStart="/home/alice/.local/bin/paperclipai" run --instance "team-a"'); expect(unit).toContain("Restart=always"); expect(unit).toContain("TimeoutStopSec=300"); diff --git a/cli/src/services/service-manager.ts b/cli/src/services/service-manager.ts index 5267d5e1451..df4037b4293 100644 --- a/cli/src/services/service-manager.ts +++ b/cli/src/services/service-manager.ts @@ -134,6 +134,7 @@ StartLimitBurst=5 [Service] Type=notify NotifyAccess=all +KillMode=mixed ExecStart="${escapeSystemd(input.shimPath)}" run --instance "${escapeSystemd(input.instanceId)}" Environment="PAPERCLIP_SERVICE_MANAGED=1" Environment="PAPERCLIP_INSTANCE_ID=${escapeSystemd(input.instanceId)}"