agent-tempo 1.5.0 → 1.5.1
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/dashboard/package.json +1 -1
- package/dist/cli/commands.js +11 -0
- package/dist/cli/config-command.d.ts +15 -0
- package/dist/cli/config-command.js +22 -7
- package/dist/client/core.js +15 -0
- package/dist/client/interface.d.ts +9 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +9 -0
- package/dist/http/server.js +12 -1
- package/dist/spawn.d.ts +10 -0
- package/dist/spawn.js +7 -0
- package/dist/tui/index.js +1 -0
- package/dist/utils/parent-death-watchdog.d.ts +12 -0
- package/dist/utils/parent-death-watchdog.js +25 -0
- package/package.json +1 -1
package/dashboard/package.json
CHANGED
package/dist/cli/commands.js
CHANGED
|
@@ -345,6 +345,10 @@ async function applyLineupPlayersAndSchedules(args) {
|
|
|
345
345
|
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
346
346
|
isConductor: false,
|
|
347
347
|
workDir: playerWorkDir,
|
|
348
|
+
// #672 — a `up --lineup` copilot PLAYER is also spawned directly (no
|
|
349
|
+
// terminal) by the transient CLI → same self-kill bug as the conductor.
|
|
350
|
+
// Skip the ppid-poll; daemon-recruit copilot (outbox.ts) keeps it.
|
|
351
|
+
transientSpawner: true,
|
|
348
352
|
});
|
|
349
353
|
}
|
|
350
354
|
else {
|
|
@@ -589,6 +593,9 @@ async function start(opts) {
|
|
|
589
593
|
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
590
594
|
isConductor: opts.conductor,
|
|
591
595
|
workDir,
|
|
596
|
+
// #672 — CLI-direct copilot spawn (start path): transient `up`/`conduct`
|
|
597
|
+
// spawner → skip the ppid-poll. Daemon-recruit copilot (outbox.ts) omits it.
|
|
598
|
+
transientSpawner: true,
|
|
592
599
|
});
|
|
593
600
|
out.success(`Launched copilot bridge "${sessionName}" (pid ${pid ?? 'unknown'})`);
|
|
594
601
|
}
|
|
@@ -1304,6 +1311,10 @@ async function up(opts) {
|
|
|
1304
1311
|
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
1305
1312
|
isConductor: true,
|
|
1306
1313
|
workDir: process.cwd(),
|
|
1314
|
+
// #672 — the `up` CLI is a TRANSIENT spawner; the detached bridge must NOT
|
|
1315
|
+
// ppid-poll it (would self-kill seconds after launch → lease never renews →
|
|
1316
|
+
// all players detach). The daemon-recruit path (outbox.ts) omits this.
|
|
1317
|
+
transientSpawner: true,
|
|
1307
1318
|
}));
|
|
1308
1319
|
}
|
|
1309
1320
|
else if (conductorAgent === 'pi') {
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import type { AgentType } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Agents valid as a persistent `defaultAgent` — the conductor-capable PRODUCTION
|
|
4
|
+
* agents. `defaultAgent` drives the conductor that `up` / `start` / `conduct`
|
|
5
|
+
* spawn when no `--agent` is given (`cli.ts` `resolvedAgent`), and the
|
|
6
|
+
* conductor-spawn branch only realises `copilot` / `pi` / else→`claude`. So:
|
|
7
|
+
* - `mock` is DEV-ONLY (recruit pre-flight rejects it outside dev mode) — never
|
|
8
|
+
* a persistent default.
|
|
9
|
+
* - the headless adapters (`claude-api` / `opencode` / `claude-code-headless`)
|
|
10
|
+
* can't be a conductor — they'd silently fall through to `claude` — so they
|
|
11
|
+
* are not offered here.
|
|
12
|
+
* Single source of truth for the interactive selector + `config set` validation
|
|
13
|
+
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
14
|
+
*/
|
|
15
|
+
export declare const VALID_DEFAULT_AGENTS: readonly AgentType[];
|
|
1
16
|
/** Interactive config setup: `agent-tempo config` */
|
|
2
17
|
export declare function configInteractive(): Promise<void>;
|
|
3
18
|
/** Non-interactive: `agent-tempo config set <key> <value>` */
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.VALID_DEFAULT_AGENTS = void 0;
|
|
36
37
|
exports.configInteractive = configInteractive;
|
|
37
38
|
exports.configSet = configSet;
|
|
38
39
|
exports.configShow = configShow;
|
|
@@ -41,6 +42,20 @@ const readline = __importStar(require("readline"));
|
|
|
41
42
|
const config_1 = require("../config");
|
|
42
43
|
const config_2 = require("../config");
|
|
43
44
|
const out = __importStar(require("./output"));
|
|
45
|
+
/**
|
|
46
|
+
* Agents valid as a persistent `defaultAgent` — the conductor-capable PRODUCTION
|
|
47
|
+
* agents. `defaultAgent` drives the conductor that `up` / `start` / `conduct`
|
|
48
|
+
* spawn when no `--agent` is given (`cli.ts` `resolvedAgent`), and the
|
|
49
|
+
* conductor-spawn branch only realises `copilot` / `pi` / else→`claude`. So:
|
|
50
|
+
* - `mock` is DEV-ONLY (recruit pre-flight rejects it outside dev mode) — never
|
|
51
|
+
* a persistent default.
|
|
52
|
+
* - the headless adapters (`claude-api` / `opencode` / `claude-code-headless`)
|
|
53
|
+
* can't be a conductor — they'd silently fall through to `claude` — so they
|
|
54
|
+
* are not offered here.
|
|
55
|
+
* Single source of truth for the interactive selector + `config set` validation
|
|
56
|
+
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
57
|
+
*/
|
|
58
|
+
exports.VALID_DEFAULT_AGENTS = ['claude', 'copilot', 'pi'];
|
|
44
59
|
// NOTE: `createTemporalConnection` is dynamic-imported inside `configInteractive`'s
|
|
45
60
|
// connection-test step (issue #157 PR C). Top-level static import would pull in
|
|
46
61
|
// `@temporalio/client`, defeating the crash-proof property of `config show` /
|
|
@@ -131,11 +146,11 @@ async function configInteractive() {
|
|
|
131
146
|
config.temporalTlsKeyPath = await ask('TLS key path', existing.temporalTlsKeyPath);
|
|
132
147
|
}
|
|
133
148
|
// Default agent type
|
|
134
|
-
const agentChoice = await choose('Default agent', [
|
|
135
|
-
if (agentChoice
|
|
136
|
-
config.defaultAgent =
|
|
149
|
+
const agentChoice = await choose('Default agent', [...exports.VALID_DEFAULT_AGENTS]);
|
|
150
|
+
if (agentChoice !== 'claude') {
|
|
151
|
+
config.defaultAgent = agentChoice;
|
|
137
152
|
}
|
|
138
|
-
// Don't set defaultAgent if claude — it's the default, keeps config clean
|
|
153
|
+
// Don't set defaultAgent if claude — it's the implicit default, keeps config clean
|
|
139
154
|
(0, config_1.saveConfigFile)(config);
|
|
140
155
|
out.success(`Saved to ${config_1.CONFIG_FILE_PATH}`);
|
|
141
156
|
// Test connection
|
|
@@ -190,9 +205,9 @@ function configSet(key, value) {
|
|
|
190
205
|
out.log(` Valid keys: ${Object.keys(keyMap).join(', ')}`);
|
|
191
206
|
process.exit(1);
|
|
192
207
|
}
|
|
193
|
-
// Validate agent type
|
|
194
|
-
if (configKey === 'defaultAgent' && value
|
|
195
|
-
out.error(`Invalid agent type: "${value}". Must be
|
|
208
|
+
// Validate agent type — restrict to the conductor-capable production agents.
|
|
209
|
+
if (configKey === 'defaultAgent' && !exports.VALID_DEFAULT_AGENTS.includes(value)) {
|
|
210
|
+
out.error(`Invalid agent type: "${value}". Must be one of: ${exports.VALID_DEFAULT_AGENTS.join(', ')}.`);
|
|
196
211
|
process.exit(1);
|
|
197
212
|
}
|
|
198
213
|
config[configKey] = value;
|
package/dist/client/core.js
CHANGED
|
@@ -1167,6 +1167,21 @@ function createTempoClientCore(client, opts = {}) {
|
|
|
1167
1167
|
return false;
|
|
1168
1168
|
}
|
|
1169
1169
|
},
|
|
1170
|
+
async ensembleExists(ensemble) {
|
|
1171
|
+
// #673 — STRONGLY-CONSISTENT existence check. `describe()` the per-ensemble
|
|
1172
|
+
// maestro HUB (started at `up`/creation via `ensureMaestroWorkflow`) — it
|
|
1173
|
+
// reflects a just-started workflow IMMEDIATELY, unlike `listEnsembles`
|
|
1174
|
+
// (Temporal visibility, eventually consistent on Cloud). Only RUNNING
|
|
1175
|
+
// counts as "exists": a TERMINATED/COMPLETED hub (destroyed ensemble) → false,
|
|
1176
|
+
// and a never-created hub throws WorkflowNotFoundError → false.
|
|
1177
|
+
try {
|
|
1178
|
+
const desc = await handle((0, config_1.maestroWorkflowId)(ensemble)).describe();
|
|
1179
|
+
return desc.status.name === 'RUNNING';
|
|
1180
|
+
}
|
|
1181
|
+
catch {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1170
1185
|
// ── Maestro session (TUI-owned workflow for two-way messaging) ──
|
|
1171
1186
|
async ensureMaestroSession(ensemble) {
|
|
1172
1187
|
const workflowId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
@@ -428,6 +428,15 @@ export interface TempoClientCore {
|
|
|
428
428
|
isConnected(): Promise<boolean>;
|
|
429
429
|
/** Check if the Global Maestro workflow is running. */
|
|
430
430
|
hasGlobalMaestro(): Promise<boolean>;
|
|
431
|
+
/**
|
|
432
|
+
* #673 — STRONGLY-CONSISTENT existence check for an ensemble: `describe()` the
|
|
433
|
+
* per-ensemble maestro hub workflow (started at `up`/creation) and report
|
|
434
|
+
* whether it's RUNNING. Unlike {@link listEnsembles} (Temporal VISIBILITY,
|
|
435
|
+
* eventually consistent on Cloud), `describe` reflects a just-started workflow
|
|
436
|
+
* immediately — the SSE existence gate uses it as a fallback so a fresh
|
|
437
|
+
* ensemble isn't 404'd before visibility catches up.
|
|
438
|
+
*/
|
|
439
|
+
ensembleExists(ensemble: string): Promise<boolean>;
|
|
431
440
|
/**
|
|
432
441
|
* Subscribe to the per-ensemble SSE event stream exposed by the daemon
|
|
433
442
|
* at `/v1/events/:ensemble`. Returns an `AsyncIterable<TempoEvent>` —
|
package/dist/config.d.ts
CHANGED
|
@@ -121,6 +121,15 @@ export declare const ENV: {
|
|
|
121
121
|
* module loads (see `src/cli/dev-mode-bootstrap.ts`).
|
|
122
122
|
*/
|
|
123
123
|
readonly DEV_MODE: "AGENT_TEMPO_DEV_MODE";
|
|
124
|
+
/**
|
|
125
|
+
* #672 — set to `'1'` by a TRANSIENT-CLI spawner (e.g. the short-lived `up`
|
|
126
|
+
* conductor) on a process it intentionally DETACHES to outlive that spawner.
|
|
127
|
+
* Tells the parent-death watchdog to skip ONLY the ppid-poll signal (which
|
|
128
|
+
* would otherwise self-kill the detached process when the transient spawner
|
|
129
|
+
* exits); the universally-correct stdin-EOF signal stays. Daemon-recruit
|
|
130
|
+
* spawns do NOT set it, so recruited adapters keep the #604 anti-leak ppid-poll.
|
|
131
|
+
*/
|
|
132
|
+
readonly NO_PPID_WATCHDOG: "AGENT_TEMPO_NO_PPID_WATCHDOG";
|
|
124
133
|
/**
|
|
125
134
|
* Escape hatch for triple-isolated environments (ADR 0014 §5.3). When
|
|
126
135
|
* set, `resolveTempoHome()` returns this path verbatim — bypassing both
|
package/dist/config.js
CHANGED
|
@@ -152,6 +152,15 @@ exports.ENV = {
|
|
|
152
152
|
* module loads (see `src/cli/dev-mode-bootstrap.ts`).
|
|
153
153
|
*/
|
|
154
154
|
DEV_MODE: 'AGENT_TEMPO_DEV_MODE',
|
|
155
|
+
/**
|
|
156
|
+
* #672 — set to `'1'` by a TRANSIENT-CLI spawner (e.g. the short-lived `up`
|
|
157
|
+
* conductor) on a process it intentionally DETACHES to outlive that spawner.
|
|
158
|
+
* Tells the parent-death watchdog to skip ONLY the ppid-poll signal (which
|
|
159
|
+
* would otherwise self-kill the detached process when the transient spawner
|
|
160
|
+
* exits); the universally-correct stdin-EOF signal stays. Daemon-recruit
|
|
161
|
+
* spawns do NOT set it, so recruited adapters keep the #604 anti-leak ppid-poll.
|
|
162
|
+
*/
|
|
163
|
+
NO_PPID_WATCHDOG: 'AGENT_TEMPO_NO_PPID_WATCHDOG',
|
|
155
164
|
/**
|
|
156
165
|
* Escape hatch for triple-isolated environments (ADR 0014 §5.3). When
|
|
157
166
|
* set, `resolveTempoHome()` returns this path verbatim — bypassing both
|
package/dist/http/server.js
CHANGED
|
@@ -546,9 +546,20 @@ async function handle(req, res, ctx) {
|
|
|
546
546
|
}
|
|
547
547
|
// Validate existence before opening the SSE stream — clean 404 when
|
|
548
548
|
// the ensemble was never live, instead of an empty stream.
|
|
549
|
+
//
|
|
550
|
+
// #673 — `listEnsembles` is a Temporal VISIBILITY query (eventually
|
|
551
|
+
// consistent; ~seconds behind on Temporal Cloud), so immediately after
|
|
552
|
+
// `up`/creation the just-started maestro hub isn't indexed yet and this
|
|
553
|
+
// gate would 404 — which the subscribe client classes as PERMANENT, leaving
|
|
554
|
+
// the TUI stuck on "Loading messages…". Before 404'ing, fall back to a
|
|
555
|
+
// STRONGLY-CONSISTENT describe of the maestro hub (`ensembleExists`): a
|
|
556
|
+
// RUNNING hub means the ensemble is live even if visibility hasn't caught up.
|
|
549
557
|
const list = await ctx.client.listEnsembles().catch(() => []);
|
|
550
558
|
if (!list.find((e) => e.name === ensemble)) {
|
|
551
|
-
|
|
559
|
+
const existsStrong = await ctx.client.ensembleExists(ensemble).catch(() => false);
|
|
560
|
+
if (!existsStrong) {
|
|
561
|
+
return (0, responses_1.errorResponse)(res, 404, { error: 'ensemble-not-found', ensemble });
|
|
562
|
+
}
|
|
552
563
|
}
|
|
553
564
|
const bus = ctx.aggregate.getOrCreateEnsembleBus(ensemble);
|
|
554
565
|
return (0, sse_handler_1.handleSseRequest)(req, res, {
|
package/dist/spawn.d.ts
CHANGED
|
@@ -150,6 +150,16 @@ export interface CopilotBridgeOpts {
|
|
|
150
150
|
attachmentId?: string;
|
|
151
151
|
attachmentRunId?: string;
|
|
152
152
|
adapterId?: string;
|
|
153
|
+
/**
|
|
154
|
+
* #672 — set true by a TRANSIENT-CLI spawner that launches this bridge DETACHED
|
|
155
|
+
* to outlive it: BOTH the `up` conductor (commands.ts) AND the `up --lineup`
|
|
156
|
+
* copilot PLAYER loop (commands.ts applyLineupPlayersAndSchedules) — both spawn
|
|
157
|
+
* the bridge directly (no terminal), so its ppid is the short-lived CLI. When
|
|
158
|
+
* set, the bridge skips the ppid-poll that would self-kill it on the CLI's exit
|
|
159
|
+
* (stdin-EOF stays). The DAEMON-recruit path (outbox.ts) OMITS it → the bridge
|
|
160
|
+
* keeps the ppid-poll (#604 anti-leak on daemon death; ppid = persistent daemon).
|
|
161
|
+
*/
|
|
162
|
+
transientSpawner?: boolean;
|
|
153
163
|
}
|
|
154
164
|
export interface CopilotBridgeResult {
|
|
155
165
|
pid: number | undefined;
|
package/dist/spawn.js
CHANGED
|
@@ -518,6 +518,10 @@ function buildPiConductorSpawn(opts) {
|
|
|
518
518
|
[config_1.ENV.TASK_QUEUE]: opts.taskQueue,
|
|
519
519
|
[config_1.ENV.ENSEMBLE]: opts.ensemble,
|
|
520
520
|
[config_1.ENV.CONDUCTOR]: 'true', // codebase-consistent; the Pi extension accepts '1'|'true'
|
|
521
|
+
// #672 — the Pi conductor is launched detached by the transient `up` CLI:
|
|
522
|
+
// skip the ppid-poll (no current pi process installs the watchdog, but this is
|
|
523
|
+
// propagation-safe + principled if a pi subprocess ever does; stdin-EOF stays).
|
|
524
|
+
[config_1.ENV.NO_PPID_WATCHDOG]: '1',
|
|
521
525
|
[config_1.ENV.PLAYER_NAME]: opts.sessionName,
|
|
522
526
|
...(opts.devMode ? { [config_1.ENV.DEV_MODE]: '1' } : {}),
|
|
523
527
|
...(opts.anthropicApiKey ? { ANTHROPIC_API_KEY: opts.anthropicApiKey } : {}),
|
|
@@ -565,6 +569,9 @@ function spawnCopilotBridge(opts) {
|
|
|
565
569
|
[config_1.ENV.BRIDGE_MODE]: '', // Clear parent's bridge mode
|
|
566
570
|
[config_1.ENV.TEMPORAL_ADDRESS]: opts.temporalAddress,
|
|
567
571
|
[config_1.ENV.CONDUCTOR]: opts.isConductor ? 'true' : '',
|
|
572
|
+
// #672 — transient-CLI spawner: the detached bridge skips the ppid-poll
|
|
573
|
+
// (would self-kill on the short-lived `up` exit). Daemon recruit omits it.
|
|
574
|
+
...(opts.transientSpawner ? { [config_1.ENV.NO_PPID_WATCHDOG]: '1' } : {}),
|
|
568
575
|
// Forward Temporal connection settings so child processes can connect
|
|
569
576
|
...(opts.temporalNamespace ? { [config_1.ENV.TEMPORAL_NAMESPACE]: opts.temporalNamespace } : {}),
|
|
570
577
|
...(opts.temporalApiKey ? { [config_1.ENV.TEMPORAL_API_KEY]: opts.temporalApiKey } : {}),
|
package/dist/tui/index.js
CHANGED
|
@@ -134,6 +134,7 @@ function createDummyClient() {
|
|
|
134
134
|
restore: fail,
|
|
135
135
|
isConnected: async () => false,
|
|
136
136
|
hasGlobalMaestro: async () => false,
|
|
137
|
+
ensembleExists: async () => false,
|
|
137
138
|
getSchedules: async () => [],
|
|
138
139
|
cancelSchedule: fail,
|
|
139
140
|
getEnsembleChat: async () => ({ messages: [], total: 0, hasMore: false, hasConductor: false }),
|
|
@@ -1 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Should the ppid-poll signal be installed? FALSE only when a TRANSIENT-CLI
|
|
3
|
+
* spawner set {@link ENV.NO_PPID_WATCHDOG} on a process it intentionally detached
|
|
4
|
+
* to OUTLIVE it (#672 — e.g. the short-lived `up` conductor: polling its dead pid
|
|
5
|
+
* would self-kill the conductor seconds after launch). Pure + injectable.
|
|
6
|
+
*
|
|
7
|
+
* Skipping ppid-poll is propagation-SAFE: the flag inherits down the spawn tree,
|
|
8
|
+
* but stdin-EOF (always installed) protects any child — its stdin IS this
|
|
9
|
+
* process's pipe, so it fires the instant THIS process dies. Only the ppid-poll
|
|
10
|
+
* (which keys on the SPAWNER, not the immediate parent) is the harmful signal.
|
|
11
|
+
*/
|
|
12
|
+
export declare function shouldInstallPpidPoll(env?: NodeJS.ProcessEnv): boolean;
|
|
1
13
|
export declare function installParentDeathWatchdog(): void;
|
|
@@ -23,15 +23,40 @@
|
|
|
23
23
|
// we falsely conclude the parent is alive. The stdin EOF path catches
|
|
24
24
|
// that case immediately, so this is purely a fallback.
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.shouldInstallPpidPoll = shouldInstallPpidPoll;
|
|
26
27
|
exports.installParentDeathWatchdog = installParentDeathWatchdog;
|
|
28
|
+
const config_1 = require("../config");
|
|
27
29
|
const log = (...args) => console.error('[agent-tempo:watchdog]', ...args);
|
|
30
|
+
/**
|
|
31
|
+
* Should the ppid-poll signal be installed? FALSE only when a TRANSIENT-CLI
|
|
32
|
+
* spawner set {@link ENV.NO_PPID_WATCHDOG} on a process it intentionally detached
|
|
33
|
+
* to OUTLIVE it (#672 — e.g. the short-lived `up` conductor: polling its dead pid
|
|
34
|
+
* would self-kill the conductor seconds after launch). Pure + injectable.
|
|
35
|
+
*
|
|
36
|
+
* Skipping ppid-poll is propagation-SAFE: the flag inherits down the spawn tree,
|
|
37
|
+
* but stdin-EOF (always installed) protects any child — its stdin IS this
|
|
38
|
+
* process's pipe, so it fires the instant THIS process dies. Only the ppid-poll
|
|
39
|
+
* (which keys on the SPAWNER, not the immediate parent) is the harmful signal.
|
|
40
|
+
*/
|
|
41
|
+
function shouldInstallPpidPoll(env = process.env) {
|
|
42
|
+
return env[config_1.ENV.NO_PPID_WATCHDOG] !== '1';
|
|
43
|
+
}
|
|
28
44
|
function installParentDeathWatchdog() {
|
|
29
45
|
const exit = (reason) => {
|
|
30
46
|
log('parent gone (', reason, ') — exiting');
|
|
31
47
|
process.exit(0);
|
|
32
48
|
};
|
|
49
|
+
// stdin-EOF — UNIVERSALLY correct + ALWAYS installed: a closed stdin pipe means
|
|
50
|
+
// the IMMEDIATE parent is gone. This is what reaps a detached process's OWN
|
|
51
|
+
// children even when ppid-poll is skipped (the child's stdin is our pipe).
|
|
33
52
|
process.stdin.on('end', () => exit('stdin end'));
|
|
34
53
|
process.stdin.on('close', () => exit('stdin close'));
|
|
54
|
+
// ppid-poll — keys on the SPAWNER's death. Correct for a long-lived daemon
|
|
55
|
+
// spawner (#604 anti-leak), HARMFUL for a transient CLI that detached us to
|
|
56
|
+
// outlive it (#672). Skipped when the spawner marked itself transient; the
|
|
57
|
+
// Temporal lease TTL reaps a genuinely-orphaned detached process instead.
|
|
58
|
+
if (!shouldInstallPpidPoll())
|
|
59
|
+
return;
|
|
35
60
|
const parentPid = process.ppid;
|
|
36
61
|
if (parentPid && parentPid > 1) {
|
|
37
62
|
const timer = setInterval(() => {
|