pikiloom 0.4.28 → 0.4.29

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.
@@ -521,7 +521,6 @@ export class DingtalkBot extends Bot {
521
521
  this.processRuntimeCleanup?.();
522
522
  this.processRuntimeCleanup = registerProcessRuntime({
523
523
  label: 'dingtalk',
524
- getActiveTaskCount: () => this.activeTasks.size,
525
524
  prepareForRestart: () => this.cleanupRuntimeForExit(),
526
525
  });
527
526
  this.installSignalHandlers();
@@ -520,7 +520,6 @@ export class DiscordBot extends Bot {
520
520
  this.processRuntimeCleanup?.();
521
521
  this.processRuntimeCleanup = registerProcessRuntime({
522
522
  label: 'discord',
523
- getActiveTaskCount: () => this.activeTasks.size,
524
523
  prepareForRestart: () => this.cleanupRuntimeForExit(),
525
524
  });
526
525
  this.installSignalHandlers();
@@ -1151,7 +1151,6 @@ export class FeishuBot extends Bot {
1151
1151
  this.processRuntimeCleanup?.();
1152
1152
  this.processRuntimeCleanup = registerProcessRuntime({
1153
1153
  label: 'feishu',
1154
- getActiveTaskCount: () => this.activeTasks.size,
1155
1154
  prepareForRestart: () => this.cleanupRuntimeForExit(),
1156
1155
  buildRestartEnv: () => this.buildRestartEnv(),
1157
1156
  });
@@ -522,7 +522,6 @@ export class SlackBot extends Bot {
522
522
  this.processRuntimeCleanup?.();
523
523
  this.processRuntimeCleanup = registerProcessRuntime({
524
524
  label: 'slack',
525
- getActiveTaskCount: () => this.activeTasks.size,
526
525
  prepareForRestart: () => this.cleanupRuntimeForExit(),
527
526
  });
528
527
  this.installSignalHandlers();
@@ -1189,7 +1189,6 @@ export class TelegramBot extends Bot {
1189
1189
  this.processRuntimeCleanup?.();
1190
1190
  this.processRuntimeCleanup = registerProcessRuntime({
1191
1191
  label: 'telegram',
1192
- getActiveTaskCount: () => this.activeTasks.size,
1193
1192
  prepareForRestart: () => this.cleanupRuntimeForExit(),
1194
1193
  buildRestartEnv: () => this.buildRestartEnv(),
1195
1194
  });
@@ -530,7 +530,6 @@ export class WeComBot extends Bot {
530
530
  this.processRuntimeCleanup?.();
531
531
  this.processRuntimeCleanup = registerProcessRuntime({
532
532
  label: 'wecom',
533
- getActiveTaskCount: () => this.activeTasks.size,
534
533
  prepareForRestart: () => this.cleanupRuntimeForExit(),
535
534
  });
536
535
  this.installSignalHandlers();
@@ -886,7 +886,6 @@ export class WeixinBot extends Bot {
886
886
  this.processRuntimeCleanup?.();
887
887
  this.processRuntimeCleanup = registerProcessRuntime({
888
888
  label: 'weixin',
889
- getActiveTaskCount: () => this.activeTasks.size,
890
889
  prepareForRestart: () => this.cleanupRuntimeForExit(),
891
890
  });
892
891
  this.installSignalHandlers();
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { spawn } from 'node:child_process';
4
+ import { execFileSync, spawn } from 'node:child_process';
5
5
  import { pathContainsSegment } from './platform.js';
6
6
  import { STATE_DIR_NAME } from './constants.js';
7
7
  export const PROCESS_RESTART_EXIT_CODE = 75;
@@ -118,12 +118,77 @@ export function registerProcessRuntime(runtime) {
118
118
  export function getRegisteredRuntimeCount() {
119
119
  return runtimes.size;
120
120
  }
121
- export function getActiveTaskCount() {
122
- let total = 0;
123
- for (const runtime of runtimes.values()) {
124
- total += Math.max(0, runtime.getActiveTaskCount?.() || 0);
121
+ function readProcessParentMap() {
122
+ const children = new Map();
123
+ try {
124
+ const rows = [];
125
+ if (process.platform === 'win32') {
126
+ const out = execFileSync('wmic', ['process', 'get', 'ParentProcessId,ProcessId'], { encoding: 'utf8', windowsHide: true });
127
+ for (const line of out.split(/\r?\n/)) {
128
+ const m = line.trim().match(/^(\d+)\s+(\d+)$/);
129
+ if (m)
130
+ rows.push({ ppid: Number(m[1]), pid: Number(m[2]) });
131
+ }
132
+ }
133
+ else {
134
+ const out = execFileSync('ps', ['-Ao', 'pid=,ppid='], { encoding: 'utf8' });
135
+ for (const line of out.split('\n')) {
136
+ const m = line.trim().match(/^(\d+)\s+(\d+)$/);
137
+ if (m)
138
+ rows.push({ pid: Number(m[1]), ppid: Number(m[2]) });
139
+ }
140
+ }
141
+ for (const { pid, ppid } of rows) {
142
+ const list = children.get(ppid);
143
+ if (list)
144
+ list.push(pid);
145
+ else
146
+ children.set(ppid, [pid]);
147
+ }
148
+ }
149
+ catch { }
150
+ return children;
151
+ }
152
+ function collectDescendantPids(rootPid) {
153
+ const children = readProcessParentMap();
154
+ const descendants = [];
155
+ const seen = new Set([rootPid]);
156
+ const stack = [rootPid];
157
+ while (stack.length) {
158
+ const current = stack.pop();
159
+ for (const child of children.get(current) || []) {
160
+ if (seen.has(child))
161
+ continue;
162
+ seen.add(child);
163
+ descendants.push(child);
164
+ stack.push(child);
165
+ }
125
166
  }
126
- return total;
167
+ return descendants;
168
+ }
169
+ export async function killChildProcesses(rootPid = process.pid, opts = {}) {
170
+ const pids = collectDescendantPids(rootPid);
171
+ if (!pids.length)
172
+ return 0;
173
+ opts.log?.(`restart: terminating ${pids.length} child process(es) before restart`);
174
+ for (const pid of pids) {
175
+ try {
176
+ process.kill(pid, 'SIGTERM');
177
+ }
178
+ catch { }
179
+ }
180
+ const graceMs = opts.graceMs ?? 1500;
181
+ if (graceMs > 0)
182
+ await new Promise(resolve => setTimeout(resolve, graceMs));
183
+ for (const pid of pids) {
184
+ if (isProcessAlive(pid)) {
185
+ try {
186
+ process.kill(pid, 'SIGKILL');
187
+ }
188
+ catch { }
189
+ }
190
+ }
191
+ return pids.length;
127
192
  }
128
193
  export function createRestartStateFilePath(ownerPid = process.pid) {
129
194
  const dir = path.join(os.tmpdir(), 'pikiloom');
@@ -223,21 +288,7 @@ function spawnReplacementProcess(bin, args, env, log) {
223
288
  }
224
289
  export async function requestProcessRestart(opts = {}) {
225
290
  if (restartInFlight) {
226
- return {
227
- ok: true,
228
- restarting: true,
229
- error: null,
230
- activeTasks: 0,
231
- };
232
- }
233
- const activeTasks = getActiveTaskCount();
234
- if (activeTasks > 0) {
235
- return {
236
- ok: false,
237
- restarting: false,
238
- error: `${activeTasks} task(s) still running. Wait for them to finish or try again.`,
239
- activeTasks,
240
- };
291
+ return { ok: true, restarting: true, error: null };
241
292
  }
242
293
  restartInFlight = true;
243
294
  const log = opts.log;
@@ -245,6 +296,7 @@ export async function requestProcessRestart(opts = {}) {
245
296
  try {
246
297
  const extraEnv = collectRestartEnv();
247
298
  await prepareRuntimesForRestart(log);
299
+ await killChildProcesses(process.pid, { log });
248
300
  if (process.env.PIKILOOM_DAEMON_CHILD === '1') {
249
301
  const restartStateFile = process.env[PROCESS_RESTART_STATE_FILE_ENV];
250
302
  if (restartStateFile) {
@@ -255,22 +307,17 @@ export async function requestProcessRestart(opts = {}) {
255
307
  }
256
308
  log?.('restart: handing off to daemon supervisor');
257
309
  exit(PROCESS_RESTART_EXIT_CODE);
258
- return { ok: true, restarting: true, error: null, activeTasks: 0 };
310
+ return { ok: true, restarting: true, error: null };
259
311
  }
260
312
  const { bin, args } = buildRestartCommand(opts.argv || process.argv.slice(2), opts.restartCmd);
261
313
  log?.(`restart: spawning \`${bin} ${args.join(' ')}\``);
262
314
  spawnReplacementProcess(bin, args, buildRestartEnvForSpawn(extraEnv), log);
263
315
  exit(0);
264
- return { ok: true, restarting: true, error: null, activeTasks: 0 };
316
+ return { ok: true, restarting: true, error: null };
265
317
  }
266
318
  catch (err) {
267
319
  restartInFlight = false;
268
- return {
269
- ok: false,
270
- restarting: false,
271
- error: err instanceof Error ? err.message : String(err),
272
- activeTasks: 0,
273
- };
320
+ return { ok: false, restarting: false, error: err instanceof Error ? err.message : String(err) };
274
321
  }
275
322
  }
276
323
  export function terminateProcessTree(target, opts = {}) {
@@ -12,7 +12,7 @@ import { validateDingtalkConfig, validateDiscordConfig, validateFeishuConfig, va
12
12
  import { resolveGuiIntegrationConfig } from '../../agent/mcp/bridge.js';
13
13
  import { normalizeWeixinBaseUrl, startWeixinQrLogin, waitForWeixinQrLogin, } from '../../channels/weixin/api.js';
14
14
  import { getConfiguredRemoteCdpUrl, getManagedBrowserStatus, launchManagedBrowserSetup, } from '../../browser-profile.js';
15
- import { requestProcessRestart, getActiveTaskCount, } from '../../core/process-control.js';
15
+ import { requestProcessRestart, } from '../../core/process-control.js';
16
16
  import { getPermissionsStatus, getHostTerminalApp, isValidPermissionKey, requestPermission, } from '../platform.js';
17
17
  import { VERSION } from '../../core/version.js';
18
18
  import { runtime } from '../runtime.js';
@@ -389,14 +389,6 @@ app.post('/api/open-preferences', async (c) => {
389
389
  return c.json(result, result.ok ? 200 : 500);
390
390
  });
391
391
  app.post('/api/restart', (c) => {
392
- const activeTasks = getActiveTaskCount();
393
- if (activeTasks > 0) {
394
- return c.json({
395
- ok: false,
396
- activeTasks,
397
- error: `${activeTasks} task(s) still running — can't restart. Wait for them to finish or stop them, then retry.`,
398
- }, 409);
399
- }
400
392
  setTimeout(() => {
401
393
  void requestProcessRestart({ log: message => runtime.log(message) });
402
394
  }, 50);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {