From 4da5e5cdf2b22f5f136192eb491ffa1d795bc1a8 Mon Sep 17 00:00:00 2001 From: Nathan Shan Date: Sun, 25 Oct 2026 11:57:08 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(server):=20=E5=85=81=E8=AE=B8=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB=E7=AE=A1=E7=90=86=E4=BB=A3=E7=90=86=E5=88=87=E7=89=87?= =?UTF-8?q?=E5=86=85=E5=AD=98=E9=99=90=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过显式环境配置跳过自动 MemoryHigh 和 MemoryMax 覆盖,保持默认自动策略。 保留 systemd 作用域、清理与 OOM 识别,并说明共享切片和旧运行时覆盖的处理。 新增四种配置的服务回归验证,针对性检查通过。 --- apps/server/src/process/agentScope.test.ts | 44 +++++++++++++++++++++- apps/server/src/process/agentScope.ts | 38 ++++++++++++------- docs/user/background-service.md | 15 ++++++++ 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/apps/server/src/process/agentScope.test.ts b/apps/server/src/process/agentScope.test.ts index 44e9d6dbf4c2a..5706d6a11e7cf 100644 --- a/apps/server/src/process/agentScope.test.ts +++ b/apps/server/src/process/agentScope.test.ts @@ -55,7 +55,12 @@ const withFakeSystemd = ( { oomPolicy = true, stopFails = false, - }: { readonly oomPolicy?: boolean; readonly stopFails?: boolean } = {}, + memoryPolicy, + }: { + readonly oomPolicy?: boolean; + readonly stopFails?: boolean; + readonly memoryPolicy?: string; + } = {}, ) => Effect.gen(function* () { const bin = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-agent-scope-")); @@ -105,7 +110,11 @@ const withFakeSystemd = ( const scope = yield* make.pipe( Effect.provideService(ProcessRunner.ProcessRunner, runner), Effect.provideService(HostProcess.Platform, "linux"), - Effect.provideService(HostProcess.Environment, { PATH: bin, XDG_RUNTIME_DIR: "/run/user/1" }), + Effect.provideService(HostProcess.Environment, { + PATH: bin, + XDG_RUNTIME_DIR: "/run/user/1", + ...(memoryPolicy === undefined ? {} : { T3CODE_AGENT_SLICE_MEMORY_POLICY: memoryPolicy }), + }), Effect.provideService(FileSystem.FileSystem, { ...fileSystem, readFileString: (path, encoding) => @@ -167,6 +176,37 @@ const launchCodex = (scope: Effect.Success, threadId: string) => ); describe("AgentScope service", () => { + it.effect.each([undefined, "automatic", "host-managed", "invalid"])( + "preserves scopes and OOM handling with memory policy %s", + (memoryPolicy) => + withFakeSystemd( + ({ scope, units, calls, stateDir }) => + Effect.gen(function* () { + const first = yield* scope.wrap({ + command: "agent", + args: [], + name: "agent", + threadId: "a", + }); + expect(first.args).toContain(`--slice=${ownedAgentSlice(stateDir)}`); + expect(first.args).toContain("--property=OOMPolicy=stop"); + yield* scope.wrap({ command: "agent", args: [], name: "agent", threadId: "b" }); + const limits = calls.filter((args) => args.includes("set-property")); + expect(limits).toHaveLength(memoryPolicy === "host-managed" ? 0 : 1); + if (memoryPolicy !== "host-managed") { + expect(limits[0]).toContain("app-t3code-agents.slice"); + expect(limits[0]?.some((arg) => arg.startsWith("MemoryHigh="))).toBe(true); + expect(limits[0]?.some((arg) => arg.startsWith("MemoryMax="))).toBe(true); + } + units.set(unitOf(first), { activeState: "failed", result: "oom-kill" }); + expect(yield* scope.oomKilled("a")).toBe(true); + yield* scope.clear("a"); + expect(yield* scope.oomKilled("a")).toBe(false); + }), + memoryPolicy === undefined ? {} : { memoryPolicy }, + ), + ); + it.effect("labels a thread from its newest scope until its next session clears it", () => withFakeSystemd(({ scope, units }) => Effect.gen(function* () { diff --git a/apps/server/src/process/agentScope.ts b/apps/server/src/process/agentScope.ts index 1e54691161ae4..dca2d2706581e 100644 --- a/apps/server/src/process/agentScope.ts +++ b/apps/server/src/process/agentScope.ts @@ -172,22 +172,34 @@ export const make = Effect.gen(function* () { yield* Effect.logInfo("Agent scopes are off: no systemd user manager"); return undefined; } - const limits = agentSliceMemoryLimits(NodeOS.totalmem()); - const limited = yield* run(systemctl, [ - "--user", - "set-property", - "--runtime", - AGENT_SLICE, - `MemoryHigh=${limits.high}`, - `MemoryMax=${limits.max}`, - ]); - if (limited._tag === "None" || limited.value.code !== 0) { - yield* Effect.logWarning("Could not set memory limits on the agents slice", { - stderr: limited._tag === "Some" ? limited.value.stderr.trim() : undefined, - }); + const memoryPolicy = hostEnvironment.T3CODE_AGENT_SLICE_MEMORY_POLICY; + if ( + memoryPolicy !== undefined && + memoryPolicy !== "automatic" && + memoryPolicy !== "host-managed" + ) { + yield* Effect.logWarning("Unknown agent slice memory policy; using automatic limits"); + } + const limits = + memoryPolicy === "host-managed" ? undefined : agentSliceMemoryLimits(NodeOS.totalmem()); + if (limits !== undefined) { + const limited = yield* run(systemctl, [ + "--user", + "set-property", + "--runtime", + AGENT_SLICE, + `MemoryHigh=${limits.high}`, + `MemoryMax=${limits.max}`, + ]); + if (limited._tag === "None" || limited.value.code !== 0) { + yield* Effect.logWarning("Could not set memory limits on the agents slice", { + stderr: limited._tag === "Some" ? limited.value.stderr.trim() : undefined, + }); + } } yield* Effect.logInfo("Agent scopes are on", { slice: AGENT_SLICE, + memoryPolicy: limits === undefined ? "host-managed" : "automatic", ...limits, oomPolicy: supportsOomPolicy, }); diff --git a/docs/user/background-service.md b/docs/user/background-service.md index e93cf7a0d9662..5ae97045bd4b8 100644 --- a/docs/user/background-service.md +++ b/docs/user/background-service.md @@ -87,6 +87,21 @@ service is still installed and will start at the next login. Windows background services are not supported. +### Host-managed agent memory on Linux + +By default, T3 Code applies automatic memory limits to the systemd user slice +`app-t3code-agents.slice`. If you manage that slice's memory budget yourself, set +`T3CODE_AGENT_SLICE_MEMORY_POLICY=host-managed` in the server's environment (or +its `service.env`) and restart the server. T3 Code then leaves `MemoryHigh` and +`MemoryMax` untouched while keeping per-thread scopes, cleanup and OOM reporting. +Unset the variable or set it to `automatic` to restore automatic limits on restart. + +The slice is shared by T3 servers running as the same OS user, so configure all +of them consistently. This option does not remove runtime overrides written by +earlier starts. Apply your intended limits with `systemctl --user set-property +--runtime app-t3code-agents.slice MemoryHigh=… MemoryMax=…`, or restart the user +manager to clear runtime overrides and load your persistent slice configuration. + T3 Connect can offer service installation during setup, but the two are managed separately. Signing out of T3 Connect does not stop or uninstall the service. From 26f15ca0942781fdec2120ff5aa642e50eb5e9ab Mon Sep 17 00:00:00 2001 From: Nathan Shan Date: Sun, 25 Oct 2026 12:03:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(server):=20=E6=9B=B4=E6=AD=A3=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E5=86=85=E5=AD=98=E9=99=90=E5=88=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除重启用户管理器即可清除运行时覆盖的错误建议。 保留显式设置内存限制的操作,并说明运行时配置可能保留至重启系统。 核对 systemctl 官方文档,受影响范围检查通过。 --- docs/user/background-service.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/user/background-service.md b/docs/user/background-service.md index 5ae97045bd4b8..9e004592ca67a 100644 --- a/docs/user/background-service.md +++ b/docs/user/background-service.md @@ -99,8 +99,9 @@ Unset the variable or set it to `automatic` to restore automatic limits on resta The slice is shared by T3 servers running as the same OS user, so configure all of them consistently. This option does not remove runtime overrides written by earlier starts. Apply your intended limits with `systemctl --user set-property ---runtime app-t3code-agents.slice MemoryHigh=… MemoryMax=…`, or restart the user -manager to clear runtime overrides and load your persistent slice configuration. +--runtime app-t3code-agents.slice MemoryHigh=… MemoryMax=…`. Restarting the server +or user manager does not clear those runtime configuration files; they can remain +until the next reboot. T3 Connect can offer service installation during setup, but the two are managed separately. Signing out of T3 Connect does not stop or uninstall the service. From dc4ebf21ee9f18141f09e14aa6ba835f2dc753b1 Mon Sep 17 00:00:00 2001 From: Nathan Shan Date: Sat, 7 Nov 2026 22:04:38 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test(server):=20=E9=AA=8C=E8=AF=81=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=86=85=E5=AD=98=E7=AD=96=E7=95=A5=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E7=88=B6=E5=88=87=E7=89=87=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在既有策略矩阵中核对父切片的 CPU、I/O 权重和任务数量限制。 - 保留主机托管策略不写入子切片内存上限的回归断言。 - check:changed 通过 20 项测试及相关类型、格式和静态检查。 --- apps/server/src/process/agentScope.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/server/src/process/agentScope.test.ts b/apps/server/src/process/agentScope.test.ts index b1202b1081acd..366bc442795d6 100644 --- a/apps/server/src/process/agentScope.test.ts +++ b/apps/server/src/process/agentScope.test.ts @@ -200,6 +200,15 @@ describe("AgentScope service", () => { const limits = properties.filter((args) => args.includes("app-t3code-agents.slice")); expect(properties).toHaveLength(memoryPolicy === "host-managed" ? 1 : 2); expect(limits).toHaveLength(memoryPolicy === "host-managed" ? 0 : 1); + expect(properties).toContainEqual([ + "--user", + "set-property", + "--runtime", + "app-t3code.slice", + "CPUWeight=10", + "IOWeight=10", + "TasksMax=25%", + ]); if (memoryPolicy !== "host-managed") { expect(limits[0]).toContain("app-t3code-agents.slice"); expect(limits[0]?.some((arg) => arg.startsWith("MemoryHigh="))).toBe(true);