@yeaft/webchat-agent 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/terminal.js CHANGED
@@ -3,6 +3,8 @@ import { existsSync, chmodSync, statSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { createRequire } from 'module';
5
5
  import ctx from './context.js';
6
+ import { getRuntimePlatformInfo } from './yeaft/runtime-platform.js';
7
+ import { wrapInvocationInSystemdUserScope } from './yeaft/systemd-scope.js';
6
8
 
7
9
  // Package name of the PTY backend. We use the Homebridge prebuilt fork
8
10
  // because upstream node-pty ships no Linux prebuilds and falls back to
@@ -91,12 +93,22 @@ export async function handleTerminalCreate(msg) {
91
93
  ? `${process.env.SystemRoot || 'C:\\Windows'}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
92
94
  : (process.env.COMSPEC || 'cmd.exe')))
93
95
  : (process.env.SHELL || 'bash');
94
- const ptyProcess = pty.spawn(shell, [], {
96
+ const terminalEnv = { ...process.env };
97
+ const terminalInvocation = wrapInvocationInSystemdUserScope(
98
+ { command: shell, args: [], family: platform() === 'win32' ? 'powershell' : 'posix' },
99
+ {
100
+ runtimePlatform: getRuntimePlatformInfo(),
101
+ env: terminalEnv,
102
+ scopeId: `terminal-${terminalId}`,
103
+ scopePrefix: 'yeaft-terminal',
104
+ },
105
+ );
106
+ const ptyProcess = pty.spawn(terminalInvocation.command, terminalInvocation.args || [], {
95
107
  name: 'xterm-256color',
96
108
  cols: cols || 80,
97
109
  rows: rows || 24,
98
110
  cwd: workDir,
99
- env: process.env
111
+ env: terminalEnv
100
112
  });
101
113
 
102
114
  // 输出缓冲 - 每 16ms 批量发送
@@ -0,0 +1,88 @@
1
+ /**
2
+ * systemd-scope.js — run child processes outside the agent service cgroup.
3
+ *
4
+ * When yeaft-agent runs as a systemd user service, shell tasks inherit the
5
+ * yeaft-agent.service cgroup by default. Long-lived commands then show up as
6
+ * "left-over process" entries every time the agent service restarts. Wrapping
7
+ * shell commands in a transient user scope keeps those user workloads alive
8
+ * without polluting the agent service lifecycle.
9
+ */
10
+
11
+ import { existsSync } from 'fs';
12
+ import { delimiter, isAbsolute, join } from 'path';
13
+
14
+ const DEFAULT_SCOPE_PREFIX = 'yeaft-shell';
15
+ const UNIT_MAX_LENGTH = 180;
16
+
17
+ function hasPathSeparator(command) {
18
+ return command.includes('/') || command.includes('\\');
19
+ }
20
+
21
+ export function findExecutableOnPath(command, env = process.env) {
22
+ if (!command || typeof command !== 'string') return null;
23
+ if (hasPathSeparator(command)) return existsSync(command) ? command : null;
24
+
25
+ const pathValue = env.PATH || '';
26
+ for (const dir of pathValue.split(delimiter)) {
27
+ if (!dir) continue;
28
+ const candidate = isAbsolute(dir) ? join(dir, command) : join(process.cwd(), dir, command);
29
+ if (existsSync(candidate)) return candidate;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ export function shouldUseSystemdUserScope({ runtimePlatform, env = process.env, systemdRunPath = null } = {}) {
35
+ if (!runtimePlatform?.isLinux) return false;
36
+ if (env.YEAFT_DISABLE_SYSTEMD_SCOPE === '1') return false;
37
+
38
+ // INVOCATION_ID is set for systemd services and transient scopes. XDG_RUNTIME_DIR
39
+ // is required for `systemd-run --user` to talk to the user manager.
40
+ if (!env.INVOCATION_ID || !env.XDG_RUNTIME_DIR) return false;
41
+
42
+ const resolvedSystemdRun = systemdRunPath || findExecutableOnPath('systemd-run', env);
43
+ return !!resolvedSystemdRun;
44
+ }
45
+
46
+ export function sanitizeSystemdUnitPart(value) {
47
+ const raw = String(value || '').trim() || `${Date.now()}-${process.pid}`;
48
+ return raw
49
+ .replace(/[^A-Za-z0-9_.-]+/g, '-')
50
+ .replace(/^-+|-+$/g, '')
51
+ .slice(0, UNIT_MAX_LENGTH) || `${Date.now()}-${process.pid}`;
52
+ }
53
+
54
+ export function buildSystemdScopeName(scopeId, prefix = DEFAULT_SCOPE_PREFIX) {
55
+ const safePrefix = sanitizeSystemdUnitPart(prefix).slice(0, 48);
56
+ const safeId = sanitizeSystemdUnitPart(scopeId);
57
+ const base = `${safePrefix}-${safeId}`.slice(0, UNIT_MAX_LENGTH);
58
+ return base.endsWith('.scope') ? base : `${base}.scope`;
59
+ }
60
+
61
+ export function wrapInvocationInSystemdUserScope(invocation, {
62
+ runtimePlatform,
63
+ env = process.env,
64
+ scopeId = null,
65
+ scopePrefix = DEFAULT_SCOPE_PREFIX,
66
+ systemdRunPath = null,
67
+ } = {}) {
68
+ if (!shouldUseSystemdUserScope({ runtimePlatform, env, systemdRunPath })) {
69
+ return { ...invocation, systemdScope: null };
70
+ }
71
+
72
+ const scopeName = buildSystemdScopeName(scopeId, scopePrefix);
73
+ return {
74
+ command: systemdRunPath || findExecutableOnPath('systemd-run', env) || 'systemd-run',
75
+ args: [
76
+ '--user',
77
+ '--scope',
78
+ '--quiet',
79
+ '--collect',
80
+ `--unit=${scopeName}`,
81
+ invocation.command,
82
+ ...(invocation.args || []),
83
+ ],
84
+ family: invocation.family,
85
+ systemdScope: scopeName,
86
+ wrappedCommand: invocation.command,
87
+ };
88
+ }
@@ -149,6 +149,7 @@ export class TaskManager {
149
149
  command,
150
150
  cwd,
151
151
  pid: null,
152
+ systemdScope: null,
152
153
  platform: (runtimePlatform || this.runtimePlatform)?.platform || process.platform,
153
154
  },
154
155
  log: {
@@ -170,6 +171,7 @@ export class TaskManager {
170
171
  command,
171
172
  cwd,
172
173
  runtimePlatform: runtime,
174
+ scopeId: task.id,
173
175
  onOutput: (stream, text) => {
174
176
  const prefix = stream === 'stderr' ? '[stderr] ' : '';
175
177
  this.store.appendLog(task.sessionId, task.id, prefix ? text.split(/(\n)/).map(part => part === '\n' ? part : (part ? `${prefix}${part}` : part)).join('') : text);
@@ -191,6 +193,7 @@ export class TaskManager {
191
193
  });
192
194
 
193
195
  task.runtime.pid = runner.pid;
196
+ task.runtime.systemdScope = runner.systemdScope || null;
194
197
  this.processes.set(this.#key(task.sessionId, task.id), runner);
195
198
  this.store.writeTask(task);
196
199
  this.#emit('updated', task);
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { spawn, spawnSync } from 'child_process';
6
6
  import { buildShellInvocation, getRuntimePlatformInfo } from '../runtime-platform.js';
7
+ import { wrapInvocationInSystemdUserScope } from '../systemd-scope.js';
7
8
 
8
9
  export function buildWindowsTaskkillArgs(pid) {
9
10
  return ['/pid', String(pid), '/t', '/f'];
@@ -33,12 +34,18 @@ export function killShellProcessTree(pid, runtimePlatform, signal = 'SIGTERM') {
33
34
  }
34
35
  }
35
36
 
36
- export function startShellProcess({ command, cwd, runtimePlatform, onOutput, onExit, onError }) {
37
+ export function startShellProcess({ command, cwd, runtimePlatform, scopeId = null, onOutput, onExit, onError }) {
37
38
  const platform = runtimePlatform || getRuntimePlatformInfo();
38
- const invocation = buildShellInvocation(command, { runtimePlatform: platform });
39
+ const env = { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' };
40
+ const baseInvocation = buildShellInvocation(command, { runtimePlatform: platform });
41
+ const invocation = wrapInvocationInSystemdUserScope(baseInvocation, {
42
+ runtimePlatform: platform,
43
+ env,
44
+ scopeId,
45
+ });
39
46
  const proc = spawn(invocation.command, invocation.args, {
40
47
  cwd,
41
- env: { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' },
48
+ env,
42
49
  stdio: ['ignore', 'pipe', 'pipe'],
43
50
  detached: !platform.isWindows,
44
51
  windowsHide: true,
@@ -57,6 +64,7 @@ export function startShellProcess({ command, cwd, runtimePlatform, onOutput, onE
57
64
 
58
65
  return {
59
66
  pid: proc.pid || null,
67
+ systemdScope: invocation.systemdScope || null,
60
68
  kill(signal = 'SIGTERM') {
61
69
  return killShellProcessTree(proc.pid, platform, signal);
62
70
  },
@@ -13,6 +13,7 @@ import { spawn } from 'child_process';
13
13
  import { existsSync } from 'fs';
14
14
  import { resolve } from 'path';
15
15
  import { buildShellInvocation, getRuntimePlatformInfo } from '../runtime-platform.js';
16
+ import { wrapInvocationInSystemdUserScope } from '../systemd-scope.js';
16
17
 
17
18
  export { buildShellInvocation };
18
19
 
@@ -32,10 +33,16 @@ const MAX_TIMEOUT_MS = 600_000;
32
33
  function runCommand(command, { cwd, timeout, signal, runtimePlatform }) {
33
34
  return new Promise((resolve) => {
34
35
  const platform = runtimePlatform || getRuntimePlatformInfo();
35
- const invocation = buildShellInvocation(command, { runtimePlatform: platform });
36
+ const env = { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' };
37
+ const baseInvocation = buildShellInvocation(command, { runtimePlatform: platform });
38
+ const invocation = wrapInvocationInSystemdUserScope(baseInvocation, {
39
+ runtimePlatform: platform,
40
+ env,
41
+ scopeId: `foreground-${Date.now()}-${process.pid}`,
42
+ });
36
43
  const proc = spawn(invocation.command, invocation.args, {
37
44
  cwd,
38
- env: { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' },
45
+ env,
39
46
  stdio: ['ignore', 'pipe', 'pipe'],
40
47
  detached: !platform.isWindows,
41
48
  });