@nextclaw/service 0.1.13 → 0.1.15
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/dist/cli/commands/agent/agent-runtime.utils.js +1 -1
- package/dist/cli/commands/agent/cli-agent-runner.utils.js +1 -5
- package/dist/cli/commands/cron/services/cron-local.service.d.ts +2 -1
- package/dist/cli/commands/diagnostics/services/diagnostics-commands.service.d.ts +2 -0
- package/dist/cli/commands/diagnostics/services/diagnostics-commands.service.js +38 -14
- package/dist/cli/commands/diagnostics/utils/diagnostics-render.utils.js +6 -0
- package/dist/cli/commands/skills/skills-query.service.d.ts +3 -2
- package/dist/commands/plugin/plugin-command.utils.d.ts +2 -2
- package/dist/commands/remote/services/remote-service-control.service.js +132 -75
- package/dist/shared/services/gateway/managers/gateway-plugin.manager.d.ts +2 -1
- package/dist/shared/services/plugin/utils/plugin-runtime-bridge.utils.js +0 -1
- package/dist/shared/services/runtime/managed-service-supervisor.service.d.ts +84 -0
- package/dist/shared/services/runtime/managed-service-supervisor.service.js +269 -0
- package/dist/shared/services/runtime/runtime-command.service.d.ts +1 -0
- package/dist/shared/services/runtime/runtime-command.service.js +3 -0
- package/dist/shared/services/runtime/service-managed-startup.service.d.ts +3 -32
- package/dist/shared/services/runtime/service-managed-startup.service.js +8 -92
- package/dist/shared/services/runtime/utils/service-remote-runtime.utils.d.ts +3 -1
- package/dist/shared/services/runtime/utils/service-remote-runtime.utils.js +21 -17
- package/dist/shared/stores/managed-service-state.store.d.ts +18 -1
- package/dist/shared/types/cli.types.d.ts +14 -0
- package/package.json +19 -19
|
@@ -15,7 +15,7 @@ async function listAvailableAgentRuntimes(params) {
|
|
|
15
15
|
createRuntime: createUnusedRuntime
|
|
16
16
|
});
|
|
17
17
|
runtimeSourceByKind.set(DEFAULT_AGENT_RUNTIME_ENTRY_ID, { source: "builtin" });
|
|
18
|
-
for (const provider of new BuiltinNarpRuntimeProviderService(() => config).createProviders()) runtimeRegistry.register(provider);
|
|
18
|
+
for (const provider of new BuiltinNarpRuntimeProviderService({ loadConfig: () => config }).createProviders()) runtimeRegistry.register(provider);
|
|
19
19
|
runtimeSourceByKind.set("narp-http", { source: "builtin" });
|
|
20
20
|
runtimeSourceByKind.set("narp-stdio", { source: "builtin" });
|
|
21
21
|
const resolvedEntries = resolveAgentRuntimeEntries({ config });
|
|
@@ -30,7 +30,7 @@ function createCliHistoryInterface() {
|
|
|
30
30
|
return rl;
|
|
31
31
|
}
|
|
32
32
|
async function runCliInteractiveLoop(params) {
|
|
33
|
-
const { agentRunRequests, config, logo, metadata, sessionKey
|
|
33
|
+
const { agentRunRequests, config, logo, metadata, sessionKey } = params;
|
|
34
34
|
console.log(`${logo} Interactive mode (type exit or Ctrl+C to quit)\n`);
|
|
35
35
|
const rl = createCliHistoryInterface();
|
|
36
36
|
let running = true;
|
|
@@ -44,7 +44,6 @@ async function runCliInteractiveLoop(params) {
|
|
|
44
44
|
}
|
|
45
45
|
printAgentResponse(await dispatchPromptOverNcp({
|
|
46
46
|
config,
|
|
47
|
-
sessionManager,
|
|
48
47
|
agentRunRequests,
|
|
49
48
|
sessionKey,
|
|
50
49
|
content: trimmed,
|
|
@@ -54,7 +53,6 @@ async function runCliInteractiveLoop(params) {
|
|
|
54
53
|
}
|
|
55
54
|
async function runCliAgentCommand(params) {
|
|
56
55
|
const { config, kernel, logo, opts } = params;
|
|
57
|
-
const sessionManager = kernel.sessions;
|
|
58
56
|
await kernel.extensions.load({ config });
|
|
59
57
|
await kernel.start();
|
|
60
58
|
try {
|
|
@@ -63,7 +61,6 @@ async function runCliAgentCommand(params) {
|
|
|
63
61
|
if (opts.message) {
|
|
64
62
|
printAgentResponse(await dispatchPromptOverNcp({
|
|
65
63
|
config,
|
|
66
|
-
sessionManager,
|
|
67
64
|
agentRunRequests: kernel.agentRunRequestManager,
|
|
68
65
|
sessionKey,
|
|
69
66
|
content: opts.message,
|
|
@@ -74,7 +71,6 @@ async function runCliAgentCommand(params) {
|
|
|
74
71
|
await runCliInteractiveLoop({
|
|
75
72
|
logo,
|
|
76
73
|
config,
|
|
77
|
-
sessionManager,
|
|
78
74
|
agentRunRequests: kernel.agentRunRequestManager,
|
|
79
75
|
sessionKey,
|
|
80
76
|
metadata: sharedMetadata
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CronAddOptions } from "../../../../shared/types/cli.types.js";
|
|
2
2
|
import { CronJobView } from "../utils/cron-job.utils.js";
|
|
3
|
+
import { AutomationManager } from "@nextclaw/kernel";
|
|
3
4
|
import { CronCreateRequest } from "@nextclaw/server";
|
|
4
5
|
|
|
5
6
|
//#region src/cli/commands/cron/services/cron-local.service.d.ts
|
|
@@ -9,7 +10,7 @@ declare function createCronCreateRequest(opts: CronAddOptions): {
|
|
|
9
10
|
};
|
|
10
11
|
declare class CronLocalService {
|
|
11
12
|
private readonly automation;
|
|
12
|
-
constructor(automation?:
|
|
13
|
+
constructor(automation?: AutomationManager);
|
|
13
14
|
readonly list: (all: boolean) => CronJobView[];
|
|
14
15
|
readonly addRequest: (request: CronCreateRequest) => CronJobView;
|
|
15
16
|
readonly add: (opts: CronAddOptions) => {
|
|
@@ -3,6 +3,7 @@ import { DoctorCommandOptions, StatusCommandOptions } from "../../../../shared/t
|
|
|
3
3
|
//#region src/cli/commands/diagnostics/services/diagnostics-commands.service.d.ts
|
|
4
4
|
declare class DiagnosticsCommands {
|
|
5
5
|
private deps;
|
|
6
|
+
private readonly managedServiceSupervisor;
|
|
6
7
|
constructor(deps: {
|
|
7
8
|
logo: string;
|
|
8
9
|
});
|
|
@@ -12,6 +13,7 @@ declare class DiagnosticsCommands {
|
|
|
12
13
|
private readonly buildDoctorChecks;
|
|
13
14
|
private readonly resolveDoctorExitCode;
|
|
14
15
|
private readonly collectRuntimeStatus;
|
|
16
|
+
private readonly resolveManagedServiceStatus;
|
|
15
17
|
private readonly probeApiHealth;
|
|
16
18
|
private readonly listProviderStatuses;
|
|
17
19
|
private readonly collectRuntimeIssues;
|
|
@@ -2,6 +2,7 @@ import { isProcessRunning, resolveUiApiBase, resolveUiConfig } from "../../../..
|
|
|
2
2
|
import { managedServiceStateStore } from "../../../../shared/stores/managed-service-state.store.js";
|
|
3
3
|
import { resolveNextclawRemoteStatusSnapshot } from "../../../../commands/remote/utils/remote-runtime-support.utils.js";
|
|
4
4
|
import "../../../../commands/remote/index.js";
|
|
5
|
+
import { ManagedServiceSupervisor } from "../../../../shared/services/runtime/managed-service-supervisor.service.js";
|
|
5
6
|
import { printDoctorReport, printStatusReport } from "../utils/diagnostics-render.utils.js";
|
|
6
7
|
import { APP_NAME, getConfigPath, getWorkspacePath, hasSecretRef, loadConfig, resolveAppLogPath } from "@nextclaw/core";
|
|
7
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -9,6 +10,7 @@ import { createServer } from "node:net";
|
|
|
9
10
|
import { listBuiltinProviders } from "@nextclaw/runtime";
|
|
10
11
|
//#region src/cli/commands/diagnostics/services/diagnostics-commands.service.ts
|
|
11
12
|
var DiagnosticsCommands = class {
|
|
13
|
+
managedServiceSupervisor = new ManagedServiceSupervisor();
|
|
12
14
|
constructor(deps) {
|
|
13
15
|
this.deps = deps;
|
|
14
16
|
}
|
|
@@ -87,8 +89,8 @@ var DiagnosticsCommands = class {
|
|
|
87
89
|
},
|
|
88
90
|
{
|
|
89
91
|
name: "service-state",
|
|
90
|
-
status: report.process.staleState ? "fail" : report.process.running ? "pass" : "warn",
|
|
91
|
-
detail: report.process.running ? `PID ${report.process.pid}` : report.process.staleState ?
|
|
92
|
+
status: report.process.staleState ? "fail" : report.process.running ? report.process.lease?.missing ? "warn" : "pass" : "warn",
|
|
93
|
+
detail: report.process.running ? `PID ${report.process.pid}${report.process.lease?.missing ? " (missing lease heartbeat)" : ""}` : report.process.staleState ? `state is stale (${report.process.staleReason ?? "unknown"})` : "service not running"
|
|
92
94
|
},
|
|
93
95
|
{
|
|
94
96
|
name: "service-health",
|
|
@@ -117,16 +119,10 @@ var DiagnosticsCommands = class {
|
|
|
117
119
|
const config = loadConfig();
|
|
118
120
|
const workspacePath = getWorkspacePath(config.agents.defaults.workspace);
|
|
119
121
|
const serviceStatePath = managedServiceStateStore.path;
|
|
120
|
-
const fixActions =
|
|
121
|
-
let serviceState = managedServiceStateStore.read();
|
|
122
|
-
if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
|
|
123
|
-
managedServiceStateStore.clear();
|
|
124
|
-
fixActions.push("Cleared stale service state file.");
|
|
125
|
-
serviceState = managedServiceStateStore.read();
|
|
126
|
-
}
|
|
122
|
+
const { fixActions, liveness, serviceState } = this.resolveManagedServiceStatus({ fix: params.fix });
|
|
127
123
|
const managedByState = Boolean(serviceState);
|
|
128
|
-
const running = Boolean(serviceState &&
|
|
129
|
-
const staleState = Boolean(serviceState &&
|
|
124
|
+
const running = Boolean(serviceState && liveness.running);
|
|
125
|
+
const staleState = Boolean(serviceState && liveness.staleState);
|
|
130
126
|
const configuredUi = resolveUiConfig(config, {
|
|
131
127
|
enabled: true,
|
|
132
128
|
host: config.ui.host,
|
|
@@ -176,8 +172,15 @@ var DiagnosticsCommands = class {
|
|
|
176
172
|
pid: serviceState?.pid ?? null,
|
|
177
173
|
running,
|
|
178
174
|
staleState,
|
|
175
|
+
staleReason: liveness.staleReason,
|
|
179
176
|
orphanSuspected,
|
|
180
|
-
startedAt: serviceState?.startedAt ?? null
|
|
177
|
+
startedAt: serviceState?.startedAt ?? null,
|
|
178
|
+
lease: serviceState ? {
|
|
179
|
+
heartbeatAt: liveness.lastHeartbeatAt,
|
|
180
|
+
expired: liveness.leaseExpired,
|
|
181
|
+
missing: liveness.leaseMissing
|
|
182
|
+
} : null,
|
|
183
|
+
lastExit: serviceState?.lastExit ?? null
|
|
181
184
|
},
|
|
182
185
|
endpoints: {
|
|
183
186
|
uiUrl: managedUiUrl,
|
|
@@ -197,6 +200,22 @@ var DiagnosticsCommands = class {
|
|
|
197
200
|
exitCode: 0
|
|
198
201
|
};
|
|
199
202
|
};
|
|
203
|
+
resolveManagedServiceStatus = (params) => {
|
|
204
|
+
const fixActions = [];
|
|
205
|
+
let serviceState = managedServiceStateStore.read();
|
|
206
|
+
let liveness = this.managedServiceSupervisor.resolveStateLiveness(serviceState);
|
|
207
|
+
if (params.fix && serviceState && liveness.staleState && !liveness.processExists) {
|
|
208
|
+
managedServiceStateStore.clear();
|
|
209
|
+
fixActions.push("Cleared stale service state file.");
|
|
210
|
+
serviceState = managedServiceStateStore.read();
|
|
211
|
+
liveness = this.managedServiceSupervisor.resolveStateLiveness(serviceState);
|
|
212
|
+
} else if (params.fix && serviceState && liveness.staleState && liveness.processExists) fixActions.push("Skipped clearing stale service state because the recorded PID still exists.");
|
|
213
|
+
return {
|
|
214
|
+
fixActions,
|
|
215
|
+
liveness,
|
|
216
|
+
serviceState
|
|
217
|
+
};
|
|
218
|
+
};
|
|
200
219
|
probeApiHealth = async (url, timeoutMs = 1500) => {
|
|
201
220
|
const controller = new AbortController();
|
|
202
221
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -266,13 +285,18 @@ var DiagnosticsCommands = class {
|
|
|
266
285
|
recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
|
|
267
286
|
}
|
|
268
287
|
if (staleState) {
|
|
269
|
-
|
|
270
|
-
|
|
288
|
+
const staleDetail = serviceState?.lastExit ? ` Last exit: ${serviceState.lastExit.reason}${serviceState.lastExit.signal ? ` (${serviceState.lastExit.signal})` : ""} at ${serviceState.lastExit.exitedAt}.` : "";
|
|
289
|
+
issues.push(`Service state is stale (${params.serviceState ? "state no longer represents a live lease" : "state missing"}).${staleDetail}`);
|
|
290
|
+
recommendations.push(params.serviceState && isProcessRunning(params.serviceState.pid) ? `Run ${APP_NAME} restart to replace the stale leased process.` : `Run ${APP_NAME} status --fix to clean stale state.`);
|
|
271
291
|
}
|
|
272
292
|
if (running && managedHealth.state !== "ok") {
|
|
273
293
|
issues.push(`Managed service health check failed: ${managedHealth.detail}`);
|
|
274
294
|
recommendations.push(`Check logs at ${serviceState?.logPath ?? resolveAppLogPath("service")}.`);
|
|
275
295
|
}
|
|
296
|
+
if (running && serviceState && !serviceState.lease) {
|
|
297
|
+
issues.push("Managed service state is missing a lease heartbeat.");
|
|
298
|
+
recommendations.push(`Run ${APP_NAME} restart to refresh the managed service state contract.`);
|
|
299
|
+
}
|
|
276
300
|
if (running && serviceState?.startupState === "degraded" && managedHealth.state !== "ok") {
|
|
277
301
|
const startupHint = serviceState.startupLastProbeError ? ` (${serviceState.startupLastProbeError})` : "";
|
|
278
302
|
issues.push(`Service is in degraded startup state${startupHint}.`);
|
|
@@ -23,6 +23,12 @@ function printProcessSection(report) {
|
|
|
23
23
|
console.log(`Process: ${processLabel}`);
|
|
24
24
|
console.log(`State file: ${report.serviceStatePath} ${report.serviceStateExists ? "✓" : "✗"}`);
|
|
25
25
|
if (report.process.startedAt) console.log(`Started: ${report.process.startedAt}`);
|
|
26
|
+
if (report.process.lease?.heartbeatAt) console.log(`Last heartbeat: ${report.process.lease.heartbeatAt}${report.process.lease.expired ? " (expired)" : ""}`);
|
|
27
|
+
if (report.process.staleReason) console.log(`Stale reason: ${report.process.staleReason}`);
|
|
28
|
+
if (report.process.lastExit) {
|
|
29
|
+
const exit = report.process.lastExit;
|
|
30
|
+
console.log(`Last exit: ${exit.reason}${exit.signal ? ` ${exit.signal}` : ""}${typeof exit.code === "number" ? ` code=${exit.code}` : ""} at ${exit.exitedAt}`);
|
|
31
|
+
}
|
|
26
32
|
console.log(`Managed health: ${report.health.managed.state} (${report.health.managed.detail})`);
|
|
27
33
|
if (!report.process.running) console.log(`Configured health: ${report.health.configured.state} (${report.health.configured.detail})`);
|
|
28
34
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as _$_nextclaw_kernel0 from "@nextclaw/kernel";
|
|
1
2
|
import { LocalizedTextMap } from "@nextclaw/kernel";
|
|
2
3
|
|
|
3
4
|
//#region src/cli/commands/skills/skills-query.service.d.ts
|
|
@@ -72,11 +73,11 @@ declare class SkillsQueryService {
|
|
|
72
73
|
workdir: string;
|
|
73
74
|
query?: string;
|
|
74
75
|
scope?: string;
|
|
75
|
-
}) =>
|
|
76
|
+
}) => _$_nextclaw_kernel0.InstalledSkillsList;
|
|
76
77
|
getInstalledInfo: (params: {
|
|
77
78
|
workdir: string;
|
|
78
79
|
selector: string;
|
|
79
|
-
}) =>
|
|
80
|
+
}) => _$_nextclaw_kernel0.InstalledSkillDetail;
|
|
80
81
|
searchMarketplaceSkills: (params: {
|
|
81
82
|
apiBaseUrl?: string;
|
|
82
83
|
query?: string;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { PluginRegistry } from "@nextclaw/openclaw-compat";
|
|
2
2
|
|
|
3
3
|
//#region src/commands/plugin/plugin-command.utils.d.ts
|
|
4
|
-
declare const RESERVED_PROVIDER_IDS:
|
|
4
|
+
declare const RESERVED_PROVIDER_IDS: any;
|
|
5
5
|
declare function buildReservedPluginLoadOptions(): {
|
|
6
6
|
reservedToolNames: ("read_file" | "write_file" | "edit_file" | "list_dir" | "exec" | "web_search" | "web_fetch" | "message" | "spawn" | "sessions_list" | "sessions_history" | "memory_search" | "memory_get" | "subagents" | "gateway" | "cron")[];
|
|
7
7
|
reservedChannelIds: string[];
|
|
8
|
-
reservedProviderIds:
|
|
8
|
+
reservedProviderIds: any;
|
|
9
9
|
};
|
|
10
10
|
declare function appendPluginCapabilityLines(lines: string[], plugin: PluginRegistry["plugins"][number]): void;
|
|
11
11
|
//#endregion
|
|
@@ -1,66 +1,84 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveUiApiBase, resolveUiConfig } from "../../../shared/utils/cli.utils.js";
|
|
2
2
|
import { managedServiceStateStore } from "../../../shared/stores/managed-service-state.store.js";
|
|
3
|
+
import { ManagedServiceSupervisor } from "../../../shared/services/runtime/managed-service-supervisor.service.js";
|
|
3
4
|
import { getConfigPath, loadConfig } from "@nextclaw/core";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
//#region src/commands/remote/services/remote-service-control.service.ts
|
|
6
7
|
const FORCED_PUBLIC_UI_HOST = "0.0.0.0";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
var RemoteServiceControlService = class {
|
|
9
|
+
managedServiceSupervisor = new ManagedServiceSupervisor();
|
|
10
|
+
resolveView = (currentUi) => {
|
|
11
|
+
if (currentUi) return {
|
|
12
|
+
running: true,
|
|
13
|
+
currentProcess: true,
|
|
14
|
+
pid: process.pid,
|
|
15
|
+
uiUrl: resolveUiApiBase(currentUi.host, currentUi.port),
|
|
16
|
+
uiPort: currentUi.port
|
|
17
|
+
};
|
|
18
|
+
const serviceState = managedServiceStateStore.read();
|
|
19
|
+
const liveness = this.managedServiceSupervisor.resolveStateLiveness(serviceState);
|
|
20
|
+
const serviceRunning = Boolean(serviceState && liveness.running);
|
|
21
|
+
return {
|
|
22
|
+
running: serviceRunning,
|
|
23
|
+
currentProcess: Boolean(serviceRunning && serviceState?.pid === process.pid),
|
|
24
|
+
...serviceState?.pid ? { pid: serviceState.pid } : {},
|
|
25
|
+
...serviceState?.uiUrl ? { uiUrl: serviceState.uiUrl } : {},
|
|
26
|
+
...typeof serviceState?.uiPort === "number" ? { uiPort: serviceState.uiPort } : {}
|
|
27
|
+
};
|
|
14
28
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
running: serviceRunning,
|
|
19
|
-
currentProcess: Boolean(serviceRunning && serviceState?.pid === process.pid),
|
|
20
|
-
...serviceState?.pid ? { pid: serviceState.pid } : {},
|
|
21
|
-
...serviceState?.uiUrl ? { uiUrl: serviceState.uiUrl } : {},
|
|
22
|
-
...typeof serviceState?.uiPort === "number" ? { uiPort: serviceState.uiPort } : {}
|
|
29
|
+
control = async (action, deps) => {
|
|
30
|
+
if (deps.remoteRuntimeController) return this.controlCurrentProcessRuntime(action, deps.remoteRuntimeController);
|
|
31
|
+
return this.controlManagedService(action, deps);
|
|
23
32
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
controlCurrentProcessRuntime = async (action, controller) => {
|
|
34
|
+
if (action === "start") {
|
|
35
|
+
await controller.start();
|
|
36
|
+
return {
|
|
37
|
+
accepted: true,
|
|
38
|
+
action,
|
|
39
|
+
message: "Remote runtime started."
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (action === "stop") {
|
|
43
|
+
await controller.stop();
|
|
44
|
+
return {
|
|
45
|
+
accepted: true,
|
|
46
|
+
action,
|
|
47
|
+
message: "Remote runtime stopped."
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
await controller.restart();
|
|
32
51
|
return {
|
|
33
52
|
accepted: true,
|
|
34
53
|
action,
|
|
35
|
-
message: "Remote runtime
|
|
54
|
+
message: "Remote runtime restarted."
|
|
36
55
|
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
};
|
|
57
|
+
controlManagedService = async (action, deps) => {
|
|
58
|
+
const serviceState = this.resolveManagedServiceControlState();
|
|
59
|
+
const uiOverrides = this.resolveManagedUiOverrides();
|
|
60
|
+
if (action === "start") return this.startManagedService(action, deps, serviceState, uiOverrides);
|
|
61
|
+
if (!serviceState.running) return this.controlStoppedManagedService(action, deps, serviceState, uiOverrides);
|
|
62
|
+
if (serviceState.currentProcess) return this.controlCurrentManagedProcess(action, deps, uiOverrides);
|
|
63
|
+
return this.controlExternalManagedProcess(action, deps, uiOverrides);
|
|
64
|
+
};
|
|
65
|
+
resolveManagedServiceControlState = () => {
|
|
66
|
+
const state = managedServiceStateStore.read();
|
|
67
|
+
const liveness = this.managedServiceSupervisor.resolveStateLiveness(state);
|
|
68
|
+
const running = Boolean(state && liveness.running);
|
|
40
69
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
currentProcess: Boolean(running && state?.pid === process.pid),
|
|
71
|
+
recordedProcessExists: Boolean(state && liveness.processExists),
|
|
72
|
+
running
|
|
44
73
|
};
|
|
45
|
-
}
|
|
46
|
-
await controller.restart();
|
|
47
|
-
return {
|
|
48
|
-
accepted: true,
|
|
49
|
-
action,
|
|
50
|
-
message: "Remote runtime restarted."
|
|
51
74
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const state = managedServiceStateStore.read();
|
|
55
|
-
const running = Boolean(state && isProcessRunning(state.pid));
|
|
56
|
-
const currentProcess = Boolean(running && state?.pid === process.pid);
|
|
57
|
-
const uiOverrides = resolveManagedUiOverrides();
|
|
58
|
-
if (action === "start") {
|
|
59
|
-
if (running) return {
|
|
75
|
+
startManagedService = async (action, deps, serviceState, uiOverrides) => {
|
|
76
|
+
if (serviceState.running) return {
|
|
60
77
|
accepted: true,
|
|
61
78
|
action,
|
|
62
|
-
message: currentProcess ? "Managed service is already running for this UI." : "Managed service is already running."
|
|
79
|
+
message: serviceState.currentProcess ? "Managed service is already running for this UI." : "Managed service is already running."
|
|
63
80
|
};
|
|
81
|
+
if (serviceState.recordedProcessExists) return this.controlStaleManagedServiceProcess(action, deps, uiOverrides);
|
|
64
82
|
await deps.serviceCommands.startService({
|
|
65
83
|
uiOverrides,
|
|
66
84
|
open: false
|
|
@@ -70,8 +88,9 @@ async function controlManagedService(action, deps) {
|
|
|
70
88
|
action,
|
|
71
89
|
message: "Managed service started."
|
|
72
90
|
};
|
|
73
|
-
}
|
|
74
|
-
|
|
91
|
+
};
|
|
92
|
+
controlStoppedManagedService = async (action, deps, serviceState, uiOverrides) => {
|
|
93
|
+
if (serviceState.recordedProcessExists) return this.controlStaleManagedServiceProcess(action, deps, uiOverrides);
|
|
75
94
|
if (action === "restart") {
|
|
76
95
|
await deps.serviceCommands.startService({
|
|
77
96
|
uiOverrides,
|
|
@@ -88,49 +107,87 @@ async function controlManagedService(action, deps) {
|
|
|
88
107
|
action,
|
|
89
108
|
message: "No managed service is currently running."
|
|
90
109
|
};
|
|
91
|
-
}
|
|
92
|
-
|
|
110
|
+
};
|
|
111
|
+
controlStaleManagedServiceProcess = async (action, deps, uiOverrides) => {
|
|
112
|
+
if (action === "stop") {
|
|
113
|
+
await deps.serviceCommands.stopService();
|
|
114
|
+
return {
|
|
115
|
+
accepted: true,
|
|
116
|
+
action,
|
|
117
|
+
message: "Stale managed service process stopped."
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (action === "restart") {
|
|
121
|
+
await deps.serviceCommands.stopService();
|
|
122
|
+
await deps.serviceCommands.startService({
|
|
123
|
+
uiOverrides,
|
|
124
|
+
open: false
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
accepted: true,
|
|
128
|
+
action,
|
|
129
|
+
message: "Stale managed service process replaced."
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
await deps.serviceCommands.stopService();
|
|
133
|
+
await deps.serviceCommands.startService({
|
|
134
|
+
uiOverrides,
|
|
135
|
+
open: false
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
accepted: true,
|
|
139
|
+
action,
|
|
140
|
+
message: "Stale managed service process replaced."
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
controlCurrentManagedProcess = async (action, deps, uiOverrides) => {
|
|
93
144
|
if (action === "restart") await deps.requestManagedServiceRestart({ uiPort: uiOverrides.port ?? 55667 });
|
|
94
|
-
else
|
|
145
|
+
else launchManagedSelfControl();
|
|
95
146
|
return {
|
|
96
147
|
accepted: true,
|
|
97
148
|
action,
|
|
98
149
|
message: action === "restart" ? "Restart scheduled. This page may disconnect for a few seconds." : "Stop scheduled. This page will disconnect shortly."
|
|
99
150
|
};
|
|
100
|
-
}
|
|
101
|
-
|
|
151
|
+
};
|
|
152
|
+
controlExternalManagedProcess = async (action, deps, uiOverrides) => {
|
|
153
|
+
if (action === "stop") {
|
|
154
|
+
await deps.serviceCommands.stopService();
|
|
155
|
+
return {
|
|
156
|
+
accepted: true,
|
|
157
|
+
action,
|
|
158
|
+
message: "Managed service stopped."
|
|
159
|
+
};
|
|
160
|
+
}
|
|
102
161
|
await deps.serviceCommands.stopService();
|
|
162
|
+
await deps.serviceCommands.startService({
|
|
163
|
+
uiOverrides,
|
|
164
|
+
open: false
|
|
165
|
+
});
|
|
103
166
|
return {
|
|
104
167
|
accepted: true,
|
|
105
168
|
action,
|
|
106
|
-
message: "Managed service
|
|
169
|
+
message: "Managed service restarted."
|
|
107
170
|
};
|
|
108
|
-
}
|
|
109
|
-
await deps.serviceCommands.stopService();
|
|
110
|
-
await deps.serviceCommands.startService({
|
|
111
|
-
uiOverrides,
|
|
112
|
-
open: false
|
|
113
|
-
});
|
|
114
|
-
return {
|
|
115
|
-
accepted: true,
|
|
116
|
-
action,
|
|
117
|
-
message: "Managed service restarted."
|
|
118
171
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
enabled: true,
|
|
123
|
-
host: FORCED_PUBLIC_UI_HOST,
|
|
124
|
-
open: false,
|
|
125
|
-
port: resolveUiConfig(loadConfig(getConfigPath()), {
|
|
172
|
+
resolveManagedUiOverrides = () => {
|
|
173
|
+
return {
|
|
126
174
|
enabled: true,
|
|
127
175
|
host: FORCED_PUBLIC_UI_HOST,
|
|
128
|
-
open: false
|
|
129
|
-
|
|
176
|
+
open: false,
|
|
177
|
+
port: resolveUiConfig(loadConfig(getConfigPath()), {
|
|
178
|
+
enabled: true,
|
|
179
|
+
host: FORCED_PUBLIC_UI_HOST,
|
|
180
|
+
open: false
|
|
181
|
+
}).port
|
|
182
|
+
};
|
|
130
183
|
};
|
|
184
|
+
};
|
|
185
|
+
const remoteServiceControlService = new RemoteServiceControlService();
|
|
186
|
+
function resolveRemoteServiceView(currentUi) {
|
|
187
|
+
return remoteServiceControlService.resolveView(currentUi);
|
|
131
188
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
189
|
+
async function controlRemoteService(action, deps) {
|
|
190
|
+
return remoteServiceControlService.control(action, deps);
|
|
134
191
|
}
|
|
135
192
|
function launchManagedSelfControl(params = {}) {
|
|
136
193
|
const script = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextclawGatewayRuntime } from "../nextclaw-gateway-runtime.service.js";
|
|
2
2
|
import { Config } from "@nextclaw/core";
|
|
3
|
+
import * as _$_nextclaw_openclaw_compat0 from "@nextclaw/openclaw-compat";
|
|
3
4
|
import { PluginChannelBinding } from "@nextclaw/openclaw-compat";
|
|
4
5
|
|
|
5
6
|
//#region src/shared/services/gateway/managers/gateway-plugin.manager.d.ts
|
|
@@ -8,7 +9,7 @@ declare class GatewayPluginManager {
|
|
|
8
9
|
private gatewayHandles;
|
|
9
10
|
constructor(gateway: NextclawGatewayRuntime);
|
|
10
11
|
getChannelBindings: () => PluginChannelBinding[];
|
|
11
|
-
getUiMetadata: () =>
|
|
12
|
+
getUiMetadata: () => _$_nextclaw_openclaw_compat0.PluginUiMetadata[];
|
|
12
13
|
load: () => Promise<void>;
|
|
13
14
|
reloadForConfigChange: (params: {
|
|
14
15
|
config: Config;
|
|
@@ -17,7 +17,6 @@ function installPluginRuntimeBridge(gateway) {
|
|
|
17
17
|
await dispatcherOptions.onReplyStart?.();
|
|
18
18
|
const response = await dispatchPromptOverNcp({
|
|
19
19
|
config: gateway.configManager.loadConfig(),
|
|
20
|
-
sessionManager: gateway.sessionManager,
|
|
21
20
|
agentRunRequests: gateway.kernel.agentRunRequestManager,
|
|
22
21
|
...request
|
|
23
22
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ManagedServiceLastExit, ManagedServiceState, ManagedServiceStateStore } from "../../stores/managed-service-state.store.js";
|
|
2
|
+
import { ManagedServiceSnapshot } from "./utils/managed-service-routing.utils.js";
|
|
3
|
+
import * as NextclawCore from "@nextclaw/core";
|
|
4
|
+
import { ChildProcess } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
//#region src/shared/services/runtime/managed-service-supervisor.service.d.ts
|
|
7
|
+
type Config$1 = NextclawCore.Config;
|
|
8
|
+
type ManagedServiceStartup = {
|
|
9
|
+
child: ChildProcess;
|
|
10
|
+
logPath: string;
|
|
11
|
+
readinessTimeoutMs: number;
|
|
12
|
+
quickPhaseTimeoutMs: number;
|
|
13
|
+
extendedPhaseTimeoutMs: number;
|
|
14
|
+
snapshot: ManagedServiceSnapshot;
|
|
15
|
+
};
|
|
16
|
+
type ManagedServiceLiveness = {
|
|
17
|
+
processExists: boolean;
|
|
18
|
+
running: boolean;
|
|
19
|
+
staleState: boolean;
|
|
20
|
+
staleReason: "process-not-running" | "lease-expired" | null;
|
|
21
|
+
leaseExpired: boolean;
|
|
22
|
+
leaseMissing: boolean;
|
|
23
|
+
lastHeartbeatAt: string | null;
|
|
24
|
+
};
|
|
25
|
+
type ManagedServiceSupervisorOptions = {
|
|
26
|
+
stateStore?: ManagedServiceStateStore;
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
isProcessRunningFn?: (pid: number) => boolean;
|
|
29
|
+
heartbeatIntervalMs?: number;
|
|
30
|
+
leaseTtlMs?: number;
|
|
31
|
+
};
|
|
32
|
+
declare class ManagedServiceSupervisor {
|
|
33
|
+
private readonly stateStore;
|
|
34
|
+
private readonly now;
|
|
35
|
+
private readonly isProcessRunningFn;
|
|
36
|
+
private readonly heartbeatIntervalMs;
|
|
37
|
+
private readonly leaseTtlMs;
|
|
38
|
+
private readonly serviceStartupLogger;
|
|
39
|
+
private heartbeatTimer;
|
|
40
|
+
private lifecycleTrackingInstalled;
|
|
41
|
+
private pendingExit;
|
|
42
|
+
constructor(options?: ManagedServiceSupervisorOptions);
|
|
43
|
+
spawnManagedService: (params: {
|
|
44
|
+
appName: string;
|
|
45
|
+
config: Config$1;
|
|
46
|
+
uiConfig: {
|
|
47
|
+
host: string;
|
|
48
|
+
port: number;
|
|
49
|
+
};
|
|
50
|
+
uiUrl: string;
|
|
51
|
+
apiUrl: string;
|
|
52
|
+
healthUrl: string;
|
|
53
|
+
startupTimeoutMs?: number;
|
|
54
|
+
resolveStartupTimeoutMs: (overrideTimeoutMs: number | undefined) => number;
|
|
55
|
+
appendStartupStage: (logPath: string, message: string) => void;
|
|
56
|
+
printStartupFailureDiagnostics: (params: {
|
|
57
|
+
uiUrl: string;
|
|
58
|
+
apiUrl: string;
|
|
59
|
+
healthUrl: string;
|
|
60
|
+
logPath: string;
|
|
61
|
+
lastProbeError: string | null;
|
|
62
|
+
}) => void;
|
|
63
|
+
resolveServiceLogPath?: () => string;
|
|
64
|
+
}) => ManagedServiceStartup | null;
|
|
65
|
+
writeReadyState: (params: {
|
|
66
|
+
readinessTimeoutMs: number;
|
|
67
|
+
readiness: {
|
|
68
|
+
ready: boolean;
|
|
69
|
+
lastProbeError: string | null;
|
|
70
|
+
};
|
|
71
|
+
snapshot: ManagedServiceSnapshot;
|
|
72
|
+
}) => ManagedServiceState;
|
|
73
|
+
installCurrentProcessLifecycleTracking: () => void;
|
|
74
|
+
startHeartbeatForCurrentProcess: (pid?: number) => void;
|
|
75
|
+
stopHeartbeatForCurrentProcess: () => void;
|
|
76
|
+
resolveStateLiveness: (state: ManagedServiceState | null) => ManagedServiceLiveness;
|
|
77
|
+
private writeHeartbeat;
|
|
78
|
+
recordCurrentProcessExit: (exit: ManagedServiceLastExit) => void;
|
|
79
|
+
private stopHeartbeat;
|
|
80
|
+
private readonly createLease;
|
|
81
|
+
private resolveLeaseStatus;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { ManagedServiceSupervisor };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { createTopLevelNextclawCommandEnv } from "../../utils/top-level-nextclaw-command-env.utils.js";
|
|
2
|
+
import { resolveCliSubcommandLaunch } from "../../utils/marketplace/cli-subcommand-launch.utils.js";
|
|
3
|
+
import { isProcessRunning, resolveServiceLogPath } from "../../utils/cli.utils.js";
|
|
4
|
+
import { managedServiceStateStore } from "../../stores/managed-service-state.store.js";
|
|
5
|
+
import { writeInitialManagedServiceState, writeReadyManagedServiceState } from "./utils/service-remote-runtime.utils.js";
|
|
6
|
+
import * as NextclawCore from "@nextclaw/core";
|
|
7
|
+
import { FileLogSink } from "@nextclaw/core";
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
//#region src/shared/services/runtime/managed-service-supervisor.service.ts
|
|
12
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 2e3;
|
|
13
|
+
const DEFAULT_LEASE_TTL_MS = 1e4;
|
|
14
|
+
const SIGNAL_EXIT_CODES = {
|
|
15
|
+
SIGHUP: 129,
|
|
16
|
+
SIGINT: 130,
|
|
17
|
+
SIGTERM: 143
|
|
18
|
+
};
|
|
19
|
+
var ManagedServiceSupervisor = class {
|
|
20
|
+
stateStore;
|
|
21
|
+
now;
|
|
22
|
+
isProcessRunningFn;
|
|
23
|
+
heartbeatIntervalMs;
|
|
24
|
+
leaseTtlMs;
|
|
25
|
+
serviceStartupLogger = NextclawCore.getAppLogger("service.startup");
|
|
26
|
+
heartbeatTimer = null;
|
|
27
|
+
lifecycleTrackingInstalled = false;
|
|
28
|
+
pendingExit = null;
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.stateStore = options.stateStore ?? managedServiceStateStore;
|
|
31
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
32
|
+
this.isProcessRunningFn = options.isProcessRunningFn ?? isProcessRunning;
|
|
33
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
34
|
+
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
35
|
+
}
|
|
36
|
+
spawnManagedService = (params) => {
|
|
37
|
+
const { appName, apiUrl, appendStartupStage, config, healthUrl, printStartupFailureDiagnostics, resolveStartupTimeoutMs, startupTimeoutMs, uiConfig, uiUrl } = params;
|
|
38
|
+
const logPath = (params.resolveServiceLogPath ?? resolveServiceLogPath)();
|
|
39
|
+
new FileLogSink({ serviceLogPath: logPath }).ensureReady();
|
|
40
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
41
|
+
const readinessTimeoutMs = resolveStartupTimeoutMs(startupTimeoutMs);
|
|
42
|
+
const quickPhaseTimeoutMs = Math.min(8e3, readinessTimeoutMs);
|
|
43
|
+
const extendedPhaseTimeoutMs = Math.max(0, readinessTimeoutMs - quickPhaseTimeoutMs);
|
|
44
|
+
appendStartupStage(logPath, `start requested: ui=${uiConfig.host}:${uiConfig.port}, readinessTimeoutMs=${readinessTimeoutMs}`);
|
|
45
|
+
console.log(`Starting ${appName} background service (readiness timeout ${Math.ceil(readinessTimeoutMs / 1e3)}s)...`);
|
|
46
|
+
const cliLaunch = resolveCliSubcommandLaunch({
|
|
47
|
+
argvEntry: process.argv[1],
|
|
48
|
+
importMetaUrl: import.meta.url,
|
|
49
|
+
cliArgs: [
|
|
50
|
+
"serve",
|
|
51
|
+
"--ui-port",
|
|
52
|
+
String(uiConfig.port)
|
|
53
|
+
],
|
|
54
|
+
nodePath: process.execPath
|
|
55
|
+
});
|
|
56
|
+
const childArgs = [...process.execArgv, ...cliLaunch.args];
|
|
57
|
+
appendStartupStage(logPath, `spawning background process: ${cliLaunch.command} ${childArgs.join(" ")}`);
|
|
58
|
+
const child = spawn(cliLaunch.command, childArgs, {
|
|
59
|
+
env: createTopLevelNextclawCommandEnv(process.env),
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
detached: true,
|
|
62
|
+
windowsHide: true
|
|
63
|
+
});
|
|
64
|
+
appendStartupStage(logPath, `spawned background process pid=${child.pid ?? "unknown"}`);
|
|
65
|
+
if (!child.pid) {
|
|
66
|
+
appendStartupStage(logPath, "spawn failed: child pid missing");
|
|
67
|
+
console.error("Error: Failed to start background service.");
|
|
68
|
+
printStartupFailureDiagnostics({
|
|
69
|
+
uiUrl,
|
|
70
|
+
apiUrl,
|
|
71
|
+
healthUrl,
|
|
72
|
+
logPath,
|
|
73
|
+
lastProbeError: null
|
|
74
|
+
});
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const snapshot = {
|
|
78
|
+
pid: child.pid,
|
|
79
|
+
uiUrl,
|
|
80
|
+
apiUrl,
|
|
81
|
+
uiHost: uiConfig.host,
|
|
82
|
+
uiPort: uiConfig.port,
|
|
83
|
+
logPath
|
|
84
|
+
};
|
|
85
|
+
writeInitialManagedServiceState({
|
|
86
|
+
config,
|
|
87
|
+
lease: this.createLease(child.pid),
|
|
88
|
+
readinessTimeoutMs,
|
|
89
|
+
snapshot
|
|
90
|
+
});
|
|
91
|
+
this.serviceStartupLogger.info("runtime.process.started", {
|
|
92
|
+
runtimeKind: "managed-service",
|
|
93
|
+
childPid: child.pid,
|
|
94
|
+
uiUrl,
|
|
95
|
+
apiUrl,
|
|
96
|
+
uiHost: uiConfig.host,
|
|
97
|
+
uiPort: uiConfig.port,
|
|
98
|
+
entrypoint: `${cliLaunch.command} ${childArgs.join(" ")}`
|
|
99
|
+
});
|
|
100
|
+
this.serviceStartupLogger.info("service_state.written", {
|
|
101
|
+
runtimeKind: "managed-service",
|
|
102
|
+
childPid: child.pid,
|
|
103
|
+
statePath: this.stateStore.path,
|
|
104
|
+
uiUrl,
|
|
105
|
+
apiUrl
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
child,
|
|
109
|
+
logPath,
|
|
110
|
+
readinessTimeoutMs,
|
|
111
|
+
quickPhaseTimeoutMs,
|
|
112
|
+
extendedPhaseTimeoutMs,
|
|
113
|
+
snapshot
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
writeReadyState = (params) => {
|
|
117
|
+
return writeReadyManagedServiceState({
|
|
118
|
+
...params,
|
|
119
|
+
lease: this.createLease(params.snapshot.pid)
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
installCurrentProcessLifecycleTracking = () => {
|
|
123
|
+
if (this.lifecycleTrackingInstalled) return;
|
|
124
|
+
this.lifecycleTrackingInstalled = true;
|
|
125
|
+
this.startHeartbeatForCurrentProcess();
|
|
126
|
+
for (const signal of [
|
|
127
|
+
"SIGHUP",
|
|
128
|
+
"SIGINT",
|
|
129
|
+
"SIGTERM"
|
|
130
|
+
]) process.once(signal, () => {
|
|
131
|
+
this.pendingExit = {
|
|
132
|
+
pid: process.pid,
|
|
133
|
+
reason: "signal",
|
|
134
|
+
exitedAt: this.now().toISOString(),
|
|
135
|
+
code: SIGNAL_EXIT_CODES[signal],
|
|
136
|
+
signal
|
|
137
|
+
};
|
|
138
|
+
this.recordCurrentProcessExit(this.pendingExit);
|
|
139
|
+
this.stopHeartbeat();
|
|
140
|
+
process.exit(SIGNAL_EXIT_CODES[signal]);
|
|
141
|
+
});
|
|
142
|
+
process.once("uncaughtExceptionMonitor", (error) => {
|
|
143
|
+
this.pendingExit = {
|
|
144
|
+
pid: process.pid,
|
|
145
|
+
reason: "uncaughtException",
|
|
146
|
+
exitedAt: this.now().toISOString(),
|
|
147
|
+
message: error instanceof Error ? error.message : String(error)
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
process.once("exit", (code) => {
|
|
151
|
+
this.recordCurrentProcessExit({
|
|
152
|
+
...this.pendingExit ?? {
|
|
153
|
+
pid: process.pid,
|
|
154
|
+
reason: "exit",
|
|
155
|
+
exitedAt: this.now().toISOString()
|
|
156
|
+
},
|
|
157
|
+
code
|
|
158
|
+
});
|
|
159
|
+
this.stopHeartbeat();
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
startHeartbeatForCurrentProcess = (pid = process.pid) => {
|
|
163
|
+
if (this.heartbeatTimer) return;
|
|
164
|
+
if (!this.writeHeartbeat(pid)) return;
|
|
165
|
+
this.heartbeatTimer = setInterval(() => {
|
|
166
|
+
if (!this.writeHeartbeat(pid)) this.stopHeartbeat();
|
|
167
|
+
}, this.heartbeatIntervalMs);
|
|
168
|
+
this.heartbeatTimer.unref();
|
|
169
|
+
};
|
|
170
|
+
stopHeartbeatForCurrentProcess = () => {
|
|
171
|
+
this.stopHeartbeat();
|
|
172
|
+
};
|
|
173
|
+
resolveStateLiveness = (state) => {
|
|
174
|
+
if (!state) return {
|
|
175
|
+
processExists: false,
|
|
176
|
+
running: false,
|
|
177
|
+
staleState: false,
|
|
178
|
+
staleReason: null,
|
|
179
|
+
leaseExpired: false,
|
|
180
|
+
leaseMissing: false,
|
|
181
|
+
lastHeartbeatAt: null
|
|
182
|
+
};
|
|
183
|
+
const processExists = this.isProcessRunningFn(state.pid);
|
|
184
|
+
const leaseStatus = this.resolveLeaseStatus(state.lease);
|
|
185
|
+
if (!processExists) return {
|
|
186
|
+
processExists,
|
|
187
|
+
running: false,
|
|
188
|
+
staleState: true,
|
|
189
|
+
staleReason: "process-not-running",
|
|
190
|
+
leaseExpired: leaseStatus.expired,
|
|
191
|
+
leaseMissing: leaseStatus.missing,
|
|
192
|
+
lastHeartbeatAt: leaseStatus.heartbeatAt
|
|
193
|
+
};
|
|
194
|
+
if (leaseStatus.expired) return {
|
|
195
|
+
processExists,
|
|
196
|
+
running: false,
|
|
197
|
+
staleState: true,
|
|
198
|
+
staleReason: "lease-expired",
|
|
199
|
+
leaseExpired: true,
|
|
200
|
+
leaseMissing: false,
|
|
201
|
+
lastHeartbeatAt: leaseStatus.heartbeatAt
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
processExists,
|
|
205
|
+
running: true,
|
|
206
|
+
staleState: false,
|
|
207
|
+
staleReason: null,
|
|
208
|
+
leaseExpired: false,
|
|
209
|
+
leaseMissing: leaseStatus.missing,
|
|
210
|
+
lastHeartbeatAt: leaseStatus.heartbeatAt
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
writeHeartbeat = (pid) => {
|
|
214
|
+
let wrote = false;
|
|
215
|
+
this.stateStore.update((state) => {
|
|
216
|
+
if (state.pid !== pid) return state;
|
|
217
|
+
wrote = true;
|
|
218
|
+
const next = {
|
|
219
|
+
...state,
|
|
220
|
+
lease: {
|
|
221
|
+
...state.lease ?? this.createLease(pid),
|
|
222
|
+
ownerPid: pid,
|
|
223
|
+
heartbeatAt: this.now().toISOString(),
|
|
224
|
+
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
225
|
+
ttlMs: this.leaseTtlMs
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
delete next.lastExit;
|
|
229
|
+
return next;
|
|
230
|
+
});
|
|
231
|
+
return wrote;
|
|
232
|
+
};
|
|
233
|
+
recordCurrentProcessExit = (exit) => {
|
|
234
|
+
this.stateStore.update((state) => {
|
|
235
|
+
if (state.pid !== exit.pid) return state;
|
|
236
|
+
return {
|
|
237
|
+
...state,
|
|
238
|
+
lastExit: exit
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
stopHeartbeat = () => {
|
|
243
|
+
if (!this.heartbeatTimer) return;
|
|
244
|
+
clearInterval(this.heartbeatTimer);
|
|
245
|
+
this.heartbeatTimer = null;
|
|
246
|
+
};
|
|
247
|
+
createLease = (ownerPid) => ({
|
|
248
|
+
ownerPid,
|
|
249
|
+
heartbeatAt: this.now().toISOString(),
|
|
250
|
+
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
251
|
+
ttlMs: this.leaseTtlMs
|
|
252
|
+
});
|
|
253
|
+
resolveLeaseStatus = (lease) => {
|
|
254
|
+
if (!lease) return {
|
|
255
|
+
expired: false,
|
|
256
|
+
missing: true,
|
|
257
|
+
heartbeatAt: null
|
|
258
|
+
};
|
|
259
|
+
const heartbeatAtMs = Date.parse(lease.heartbeatAt);
|
|
260
|
+
const ttlMs = Number.isFinite(lease.ttlMs) ? lease.ttlMs : this.leaseTtlMs;
|
|
261
|
+
return {
|
|
262
|
+
expired: !Number.isFinite(heartbeatAtMs) || heartbeatAtMs + ttlMs < this.now().getTime(),
|
|
263
|
+
missing: false,
|
|
264
|
+
heartbeatAt: lease.heartbeatAt
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
269
|
+
export { ManagedServiceSupervisor };
|
|
@@ -11,6 +11,7 @@ declare class RuntimeCommandService {
|
|
|
11
11
|
private loggingInstalled;
|
|
12
12
|
private processExitLoggingInstalled;
|
|
13
13
|
private readonly runtimeLogger;
|
|
14
|
+
private readonly managedServiceSupervisor;
|
|
14
15
|
private readonly managedServiceCommandService;
|
|
15
16
|
constructor(deps: {
|
|
16
17
|
requestRestart: (params: RequestRestartParams) => Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveCliSubcommandEntry } from "../../utils/marketplace/cli-subcommand-launch.utils.js";
|
|
2
2
|
import { isLoopbackHost, resolvePublicIp, resolveUiStaticDir } from "../../utils/cli.utils.js";
|
|
3
3
|
import { NextclawDistributionService } from "./nextclaw-distribution.service.js";
|
|
4
|
+
import { ManagedServiceSupervisor } from "./managed-service-supervisor.service.js";
|
|
4
5
|
import { describeUnmanagedHealthyTargetMessage, inspectUiTarget } from "../../utils/service-port-probe.utils.js";
|
|
5
6
|
import { ManagedServiceCommandService } from "./service-managed-startup.service.js";
|
|
6
7
|
import { buildMarketplaceSkillInstallArgs, pickUserFacingCommandSummary } from "../../utils/marketplace/service-marketplace-helpers.utils.js";
|
|
@@ -14,6 +15,7 @@ var RuntimeCommandService = class {
|
|
|
14
15
|
loggingInstalled = false;
|
|
15
16
|
processExitLoggingInstalled = false;
|
|
16
17
|
runtimeLogger = NextclawCore.getAppLogger("service.runtime");
|
|
18
|
+
managedServiceSupervisor = new ManagedServiceSupervisor();
|
|
17
19
|
managedServiceCommandService = new ManagedServiceCommandService({
|
|
18
20
|
startGateway: async (options) => await this.startGateway(options),
|
|
19
21
|
printPublicUiUrls: async (host, port) => await this.printPublicUiUrls(host, port),
|
|
@@ -27,6 +29,7 @@ var RuntimeCommandService = class {
|
|
|
27
29
|
startGateway = async (options = {}) => {
|
|
28
30
|
this.ensureRuntimeLoggingInstalled();
|
|
29
31
|
this.installProcessExitLogging();
|
|
32
|
+
this.managedServiceSupervisor.installCurrentProcessLifecycleTracking();
|
|
30
33
|
this.runtimeLogger.info("runtime.process.started", {
|
|
31
34
|
runtimeKind: "serve-process",
|
|
32
35
|
pid: process.pid,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveManagedServiceReadySnapshot, resolveManagedServiceUiBinding, resolveSessionRouteCandidate } from "./utils/managed-service-routing.utils.js";
|
|
2
2
|
import * as NextclawCore from "@nextclaw/core";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
3
|
|
|
5
4
|
//#region src/shared/services/runtime/service-managed-startup.service.d.ts
|
|
6
5
|
type Config$1 = NextclawCore.Config;
|
|
@@ -9,35 +8,6 @@ type StartServiceOptions = {
|
|
|
9
8
|
open: boolean;
|
|
10
9
|
startupTimeoutMs?: number;
|
|
11
10
|
};
|
|
12
|
-
declare function spawnManagedService(params: {
|
|
13
|
-
appName: string;
|
|
14
|
-
config: NextclawCore.Config;
|
|
15
|
-
uiConfig: {
|
|
16
|
-
host: string;
|
|
17
|
-
port: number;
|
|
18
|
-
};
|
|
19
|
-
uiUrl: string;
|
|
20
|
-
apiUrl: string;
|
|
21
|
-
healthUrl: string;
|
|
22
|
-
startupTimeoutMs?: number;
|
|
23
|
-
resolveStartupTimeoutMs: (overrideTimeoutMs: number | undefined) => number;
|
|
24
|
-
appendStartupStage: (logPath: string, message: string) => void;
|
|
25
|
-
printStartupFailureDiagnostics: (params: {
|
|
26
|
-
uiUrl: string;
|
|
27
|
-
apiUrl: string;
|
|
28
|
-
healthUrl: string;
|
|
29
|
-
logPath: string;
|
|
30
|
-
lastProbeError: string | null;
|
|
31
|
-
}) => void;
|
|
32
|
-
resolveServiceLogPath: () => string;
|
|
33
|
-
}): {
|
|
34
|
-
child: ReturnType<typeof spawn>;
|
|
35
|
-
logPath: string;
|
|
36
|
-
readinessTimeoutMs: number;
|
|
37
|
-
quickPhaseTimeoutMs: number;
|
|
38
|
-
extendedPhaseTimeoutMs: number;
|
|
39
|
-
snapshot: ManagedServiceSnapshot;
|
|
40
|
-
} | null;
|
|
41
11
|
declare function waitForManagedServiceReadiness(params: {
|
|
42
12
|
appName: string;
|
|
43
13
|
childPid: number;
|
|
@@ -85,6 +55,7 @@ declare class ManagedServiceCommandService {
|
|
|
85
55
|
private readonly loggingRuntime;
|
|
86
56
|
private readonly serviceLogger;
|
|
87
57
|
private readonly startupLogger;
|
|
58
|
+
private readonly supervisor;
|
|
88
59
|
constructor(deps: {
|
|
89
60
|
startGateway: (options: {
|
|
90
61
|
uiOverrides: Partial<Config$1["ui"]>;
|
|
@@ -120,4 +91,4 @@ declare class ManagedServiceCommandService {
|
|
|
120
91
|
private printStartupFailureDiagnostics;
|
|
121
92
|
}
|
|
122
93
|
//#endregion
|
|
123
|
-
export { ManagedServiceCommandService, StartServiceOptions, reportManagedServiceStart, resolveManagedServiceReadySnapshot, resolveManagedServiceUiBinding, resolveSessionRouteCandidate,
|
|
94
|
+
export { ManagedServiceCommandService, StartServiceOptions, reportManagedServiceStart, resolveManagedServiceReadySnapshot, resolveManagedServiceUiBinding, resolveSessionRouteCandidate, waitForManagedServiceReadiness };
|
|
@@ -1,98 +1,12 @@
|
|
|
1
|
-
import { createTopLevelNextclawCommandEnv } from "../../utils/top-level-nextclaw-command-env.utils.js";
|
|
2
|
-
import { resolveCliSubcommandLaunch } from "../../utils/marketplace/cli-subcommand-launch.utils.js";
|
|
3
1
|
import { isProcessRunning, openBrowser, resolveServiceLogPath, resolveUiApiBase, resolveUiConfig, waitForExit } from "../../utils/cli.utils.js";
|
|
4
2
|
import { managedServiceStateStore } from "../../stores/managed-service-state.store.js";
|
|
5
3
|
import { localUiRuntimeStore } from "../../stores/local-ui-runtime.store.js";
|
|
6
|
-
import {
|
|
4
|
+
import { ManagedServiceSupervisor } from "./managed-service-supervisor.service.js";
|
|
7
5
|
import { resolveManagedServiceReadySnapshot, resolveManagedServiceUiBinding, resolveSessionRouteCandidate } from "./utils/managed-service-routing.utils.js";
|
|
8
6
|
import { probeHealthEndpoint } from "../../utils/service-port-probe.utils.js";
|
|
9
7
|
import * as NextclawCore from "@nextclaw/core";
|
|
10
|
-
import { FileLogSink } from "@nextclaw/core";
|
|
11
|
-
import { mkdirSync } from "node:fs";
|
|
12
|
-
import { spawn } from "node:child_process";
|
|
13
|
-
import { dirname } from "node:path";
|
|
14
8
|
//#region src/shared/services/runtime/service-managed-startup.service.ts
|
|
15
9
|
const { APP_NAME: APP_NAME$1, loadConfig: loadConfig$1 } = NextclawCore;
|
|
16
|
-
const serviceStartupLogger = NextclawCore.getAppLogger("service.startup");
|
|
17
|
-
function spawnManagedService(params) {
|
|
18
|
-
const { appName, config, uiConfig, uiUrl, apiUrl, healthUrl, startupTimeoutMs, resolveStartupTimeoutMs, appendStartupStage, printStartupFailureDiagnostics, resolveServiceLogPath } = params;
|
|
19
|
-
const logPath = resolveServiceLogPath();
|
|
20
|
-
new FileLogSink({ serviceLogPath: logPath }).ensureReady();
|
|
21
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
22
|
-
const readinessTimeoutMs = resolveStartupTimeoutMs(startupTimeoutMs);
|
|
23
|
-
const quickPhaseTimeoutMs = Math.min(8e3, readinessTimeoutMs);
|
|
24
|
-
const extendedPhaseTimeoutMs = Math.max(0, readinessTimeoutMs - quickPhaseTimeoutMs);
|
|
25
|
-
appendStartupStage(logPath, `start requested: ui=${uiConfig.host}:${uiConfig.port}, readinessTimeoutMs=${readinessTimeoutMs}`);
|
|
26
|
-
console.log(`Starting ${appName} background service (readiness timeout ${Math.ceil(readinessTimeoutMs / 1e3)}s)...`);
|
|
27
|
-
const cliLaunch = resolveCliSubcommandLaunch({
|
|
28
|
-
argvEntry: process.argv[1],
|
|
29
|
-
importMetaUrl: import.meta.url,
|
|
30
|
-
cliArgs: [
|
|
31
|
-
"serve",
|
|
32
|
-
"--ui-port",
|
|
33
|
-
String(uiConfig.port)
|
|
34
|
-
],
|
|
35
|
-
nodePath: process.execPath
|
|
36
|
-
});
|
|
37
|
-
const childArgs = [...process.execArgv, ...cliLaunch.args];
|
|
38
|
-
appendStartupStage(logPath, `spawning background process: ${cliLaunch.command} ${childArgs.join(" ")}`);
|
|
39
|
-
const child = spawn(cliLaunch.command, childArgs, {
|
|
40
|
-
env: createTopLevelNextclawCommandEnv(process.env),
|
|
41
|
-
stdio: "ignore",
|
|
42
|
-
detached: true,
|
|
43
|
-
windowsHide: true
|
|
44
|
-
});
|
|
45
|
-
appendStartupStage(logPath, `spawned background process pid=${child.pid ?? "unknown"}`);
|
|
46
|
-
if (!child.pid) {
|
|
47
|
-
appendStartupStage(logPath, "spawn failed: child pid missing");
|
|
48
|
-
console.error("Error: Failed to start background service.");
|
|
49
|
-
printStartupFailureDiagnostics({
|
|
50
|
-
uiUrl,
|
|
51
|
-
apiUrl,
|
|
52
|
-
healthUrl,
|
|
53
|
-
logPath,
|
|
54
|
-
lastProbeError: null
|
|
55
|
-
});
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const snapshot = {
|
|
59
|
-
pid: child.pid,
|
|
60
|
-
uiUrl,
|
|
61
|
-
apiUrl,
|
|
62
|
-
uiHost: uiConfig.host,
|
|
63
|
-
uiPort: uiConfig.port,
|
|
64
|
-
logPath
|
|
65
|
-
};
|
|
66
|
-
writeInitialManagedServiceState({
|
|
67
|
-
config,
|
|
68
|
-
readinessTimeoutMs,
|
|
69
|
-
snapshot
|
|
70
|
-
});
|
|
71
|
-
serviceStartupLogger.info("runtime.process.started", {
|
|
72
|
-
runtimeKind: "managed-service",
|
|
73
|
-
childPid: child.pid,
|
|
74
|
-
uiUrl,
|
|
75
|
-
apiUrl,
|
|
76
|
-
uiHost: uiConfig.host,
|
|
77
|
-
uiPort: uiConfig.port,
|
|
78
|
-
entrypoint: `${cliLaunch.command} ${childArgs.join(" ")}`
|
|
79
|
-
});
|
|
80
|
-
serviceStartupLogger.info("service_state.written", {
|
|
81
|
-
runtimeKind: "managed-service",
|
|
82
|
-
childPid: child.pid,
|
|
83
|
-
statePath: managedServiceStateStore.path,
|
|
84
|
-
uiUrl,
|
|
85
|
-
apiUrl
|
|
86
|
-
});
|
|
87
|
-
return {
|
|
88
|
-
child,
|
|
89
|
-
logPath,
|
|
90
|
-
readinessTimeoutMs,
|
|
91
|
-
quickPhaseTimeoutMs,
|
|
92
|
-
extendedPhaseTimeoutMs,
|
|
93
|
-
snapshot
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
10
|
async function waitForManagedServiceReadiness(params) {
|
|
97
11
|
params.appendStartupStage(params.logPath, `health probe started: ${params.healthUrl} (phase=quick, timeoutMs=${params.quickPhaseTimeoutMs})`);
|
|
98
12
|
let readiness = await params.waitForBackgroundServiceReady({
|
|
@@ -128,6 +42,7 @@ var ManagedServiceCommandService = class {
|
|
|
128
42
|
loggingRuntime = NextclawCore.getLoggingRuntime();
|
|
129
43
|
serviceLogger = this.loggingRuntime.getLogger("service");
|
|
130
44
|
startupLogger = this.serviceLogger.child("startup");
|
|
45
|
+
supervisor = new ManagedServiceSupervisor();
|
|
131
46
|
constructor(deps) {
|
|
132
47
|
this.deps = deps;
|
|
133
48
|
}
|
|
@@ -149,7 +64,8 @@ var ManagedServiceCommandService = class {
|
|
|
149
64
|
const apiUrl = `${uiUrl}/api`;
|
|
150
65
|
const staticDir = this.deps.resolveUiStaticDir();
|
|
151
66
|
const existing = managedServiceStateStore.read();
|
|
152
|
-
|
|
67
|
+
const existingLiveness = this.supervisor.resolveStateLiveness(existing);
|
|
68
|
+
if (existing && existingLiveness.running) {
|
|
153
69
|
await this.handleExistingManagedService({
|
|
154
70
|
existing,
|
|
155
71
|
uiConfig,
|
|
@@ -157,7 +73,7 @@ var ManagedServiceCommandService = class {
|
|
|
157
73
|
});
|
|
158
74
|
return;
|
|
159
75
|
}
|
|
160
|
-
if (existing) managedServiceStateStore.clear();
|
|
76
|
+
if (existing && !existingLiveness.processExists) managedServiceStateStore.clear();
|
|
161
77
|
if (!staticDir) {
|
|
162
78
|
process.exitCode = 1, console.error(`Error: ${APP_NAME$1} UI frontend bundle not found. Reinstall or rebuild ${APP_NAME$1}. For dev-only overrides, set NEXTCLAW_UI_STATIC_DIR to a built frontend directory.`);
|
|
163
79
|
return;
|
|
@@ -282,7 +198,7 @@ var ManagedServiceCommandService = class {
|
|
|
282
198
|
};
|
|
283
199
|
startNewManagedServiceTarget = async (params) => {
|
|
284
200
|
const { apiUrl, config, healthUrl, startupTimeoutMs, uiConfig, uiUrl } = params;
|
|
285
|
-
const startup = spawnManagedService({
|
|
201
|
+
const startup = this.supervisor.spawnManagedService({
|
|
286
202
|
appName: APP_NAME$1,
|
|
287
203
|
config,
|
|
288
204
|
uiConfig,
|
|
@@ -336,7 +252,7 @@ var ManagedServiceCommandService = class {
|
|
|
336
252
|
}
|
|
337
253
|
startup.child.unref();
|
|
338
254
|
const readySnapshot = resolveManagedServiceReadySnapshot({ snapshot: startup.snapshot });
|
|
339
|
-
const state =
|
|
255
|
+
const state = this.supervisor.writeReadyState({
|
|
340
256
|
readinessTimeoutMs: startup.readinessTimeoutMs,
|
|
341
257
|
readiness,
|
|
342
258
|
snapshot: readySnapshot
|
|
@@ -420,4 +336,4 @@ var ManagedServiceCommandService = class {
|
|
|
420
336
|
};
|
|
421
337
|
};
|
|
422
338
|
//#endregion
|
|
423
|
-
export { ManagedServiceCommandService, reportManagedServiceStart, resolveManagedServiceReadySnapshot, resolveManagedServiceUiBinding, resolveSessionRouteCandidate,
|
|
339
|
+
export { ManagedServiceCommandService, reportManagedServiceStart, resolveManagedServiceReadySnapshot, resolveManagedServiceUiBinding, resolveSessionRouteCandidate, waitForManagedServiceReadiness };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ManagedServiceState } from "../../../stores/managed-service-state.store.js";
|
|
1
|
+
import { ManagedServiceLease, ManagedServiceState } from "../../../stores/managed-service-state.store.js";
|
|
2
2
|
import { Config } from "@nextclaw/core";
|
|
3
3
|
import { RemoteRuntimeState, RemoteServiceModule } from "@nextclaw/remote";
|
|
4
4
|
|
|
@@ -40,10 +40,12 @@ declare function createManagedRemoteModuleForUi(params: {
|
|
|
40
40
|
}): RemoteServiceModule | null;
|
|
41
41
|
declare function writeInitialManagedServiceState(params: {
|
|
42
42
|
config: Config;
|
|
43
|
+
lease?: ManagedServiceLease;
|
|
43
44
|
readinessTimeoutMs: number;
|
|
44
45
|
snapshot: ManagedServiceSnapshot;
|
|
45
46
|
}): void;
|
|
46
47
|
declare function writeReadyManagedServiceState(params: {
|
|
48
|
+
lease?: ManagedServiceLease;
|
|
47
49
|
readinessTimeoutMs: number;
|
|
48
50
|
readiness: {
|
|
49
51
|
ready: boolean;
|
|
@@ -137,33 +137,37 @@ function createManagedRemoteModuleForUi(params) {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
function writeInitialManagedServiceState(params) {
|
|
140
|
+
const { config, lease, readinessTimeoutMs, snapshot } = params;
|
|
140
141
|
managedServiceStateStore.write({
|
|
141
|
-
pid:
|
|
142
|
+
pid: snapshot.pid,
|
|
142
143
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
143
|
-
uiUrl:
|
|
144
|
-
apiUrl:
|
|
145
|
-
uiHost:
|
|
146
|
-
uiPort:
|
|
147
|
-
logPath:
|
|
144
|
+
uiUrl: snapshot.uiUrl,
|
|
145
|
+
apiUrl: snapshot.apiUrl,
|
|
146
|
+
uiHost: snapshot.uiHost,
|
|
147
|
+
uiPort: snapshot.uiPort,
|
|
148
|
+
logPath: snapshot.logPath,
|
|
149
|
+
...lease ? { lease } : {},
|
|
148
150
|
startupLastProbeError: null,
|
|
149
|
-
startupTimeoutMs:
|
|
151
|
+
startupTimeoutMs: readinessTimeoutMs,
|
|
150
152
|
startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
-
...
|
|
153
|
+
...config.remote.enabled ? { remote: buildNextclawConfiguredRemoteState(config) } : {}
|
|
152
154
|
});
|
|
153
155
|
}
|
|
154
156
|
function writeReadyManagedServiceState(params) {
|
|
157
|
+
const { lease, readiness, readinessTimeoutMs, snapshot } = params;
|
|
155
158
|
const currentState = managedServiceStateStore.read();
|
|
156
159
|
const state = {
|
|
157
|
-
pid:
|
|
160
|
+
pid: snapshot.pid,
|
|
158
161
|
startedAt: currentState?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
-
uiUrl:
|
|
160
|
-
apiUrl:
|
|
161
|
-
uiHost:
|
|
162
|
-
uiPort:
|
|
163
|
-
logPath:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
uiUrl: snapshot.uiUrl,
|
|
163
|
+
apiUrl: snapshot.apiUrl,
|
|
164
|
+
uiHost: snapshot.uiHost,
|
|
165
|
+
uiPort: snapshot.uiPort,
|
|
166
|
+
logPath: snapshot.logPath,
|
|
167
|
+
...lease ? { lease } : currentState?.lease ? { lease: currentState.lease } : {},
|
|
168
|
+
startupState: readiness.ready ? "ready" : "degraded",
|
|
169
|
+
startupLastProbeError: readiness.lastProbeError,
|
|
170
|
+
startupTimeoutMs: readinessTimeoutMs,
|
|
167
171
|
startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
168
172
|
...currentState?.remote ? { remote: currentState.remote } : {}
|
|
169
173
|
};
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { RemoteRuntimeState } from "@nextclaw/remote";
|
|
2
2
|
|
|
3
3
|
//#region src/shared/stores/managed-service-state.store.d.ts
|
|
4
|
+
type ManagedServiceLease = {
|
|
5
|
+
ownerPid: number;
|
|
6
|
+
heartbeatAt: string;
|
|
7
|
+
heartbeatIntervalMs: number;
|
|
8
|
+
ttlMs: number;
|
|
9
|
+
};
|
|
10
|
+
type ManagedServiceExitReason = "exit" | "signal" | "uncaughtException";
|
|
11
|
+
type ManagedServiceLastExit = {
|
|
12
|
+
pid: number;
|
|
13
|
+
reason: ManagedServiceExitReason;
|
|
14
|
+
exitedAt: string;
|
|
15
|
+
code?: number | null;
|
|
16
|
+
signal?: string | null;
|
|
17
|
+
message?: string | null;
|
|
18
|
+
};
|
|
4
19
|
type ManagedServiceState = {
|
|
5
20
|
pid: number;
|
|
6
21
|
startedAt: string;
|
|
@@ -13,6 +28,8 @@ type ManagedServiceState = {
|
|
|
13
28
|
startupLastProbeError?: string | null;
|
|
14
29
|
startupTimeoutMs?: number;
|
|
15
30
|
startupCheckedAt?: string;
|
|
31
|
+
lease?: ManagedServiceLease;
|
|
32
|
+
lastExit?: ManagedServiceLastExit;
|
|
16
33
|
remote?: RemoteRuntimeState;
|
|
17
34
|
};
|
|
18
35
|
declare class ManagedServiceStateStore {
|
|
@@ -25,4 +42,4 @@ declare class ManagedServiceStateStore {
|
|
|
25
42
|
}
|
|
26
43
|
declare const managedServiceStateStore: ManagedServiceStateStore;
|
|
27
44
|
//#endregion
|
|
28
|
-
export { ManagedServiceState, ManagedServiceStateStore, managedServiceStateStore };
|
|
45
|
+
export { ManagedServiceLastExit, ManagedServiceLease, ManagedServiceState, ManagedServiceStateStore, managedServiceStateStore };
|
|
@@ -250,8 +250,22 @@ type RuntimeStatusReport = {
|
|
|
250
250
|
pid: number | null;
|
|
251
251
|
running: boolean;
|
|
252
252
|
staleState: boolean;
|
|
253
|
+
staleReason: "process-not-running" | "lease-expired" | null;
|
|
253
254
|
orphanSuspected: boolean;
|
|
254
255
|
startedAt: string | null;
|
|
256
|
+
lease: {
|
|
257
|
+
heartbeatAt: string | null;
|
|
258
|
+
expired: boolean;
|
|
259
|
+
missing: boolean;
|
|
260
|
+
} | null;
|
|
261
|
+
lastExit: {
|
|
262
|
+
pid: number;
|
|
263
|
+
reason: string;
|
|
264
|
+
exitedAt: string;
|
|
265
|
+
code?: number | null;
|
|
266
|
+
signal?: string | null;
|
|
267
|
+
message?: string | null;
|
|
268
|
+
} | null;
|
|
255
269
|
};
|
|
256
270
|
endpoints: {
|
|
257
271
|
uiUrl: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/service",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "NextClaw long-running service host and runtime lifecycle.",
|
|
6
6
|
"type": "module",
|
|
@@ -35,24 +35,24 @@
|
|
|
35
35
|
"chokidar": "^3.6.0",
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"jszip": "^3.10.1",
|
|
38
|
-
"@nextclaw/channel-extension-
|
|
39
|
-
"@nextclaw/channel-extension-
|
|
40
|
-
"@nextclaw/
|
|
41
|
-
"@nextclaw/
|
|
42
|
-
"@nextclaw/
|
|
43
|
-
"@nextclaw/
|
|
44
|
-
"@nextclaw/
|
|
45
|
-
"@nextclaw/ncp": "0.5.
|
|
46
|
-
"@nextclaw/ncp-
|
|
47
|
-
"@nextclaw/
|
|
48
|
-
"@nextclaw/nextclaw-ncp-runtime-
|
|
49
|
-
"@nextclaw/
|
|
50
|
-
"@nextclaw/ncp-
|
|
51
|
-
"@nextclaw/
|
|
52
|
-
"@nextclaw/
|
|
53
|
-
"@nextclaw/runtime": "0.2.
|
|
54
|
-
"@nextclaw/
|
|
55
|
-
"@nextclaw/
|
|
38
|
+
"@nextclaw/channel-extension-qq": "0.1.3",
|
|
39
|
+
"@nextclaw/channel-extension-feishu": "0.1.6",
|
|
40
|
+
"@nextclaw/channel-extension-weixin": "0.1.9",
|
|
41
|
+
"@nextclaw/core": "0.12.22",
|
|
42
|
+
"@nextclaw/kernel": "0.1.12",
|
|
43
|
+
"@nextclaw/mcp": "0.1.87",
|
|
44
|
+
"@nextclaw/ncp-agent-runtime": "0.3.26",
|
|
45
|
+
"@nextclaw/ncp": "0.5.15",
|
|
46
|
+
"@nextclaw/ncp-toolkit": "0.5.20",
|
|
47
|
+
"@nextclaw/ncp-mcp": "0.1.89",
|
|
48
|
+
"@nextclaw/nextclaw-ncp-runtime-http-client": "0.1.14",
|
|
49
|
+
"@nextclaw/nextclaw-hermes-acp-bridge": "0.1.14",
|
|
50
|
+
"@nextclaw/nextclaw-ncp-runtime-stdio-client": "0.1.15",
|
|
51
|
+
"@nextclaw/remote": "0.1.100",
|
|
52
|
+
"@nextclaw/openclaw-compat": "1.0.22",
|
|
53
|
+
"@nextclaw/runtime": "0.2.54",
|
|
54
|
+
"@nextclaw/shared": "0.1.9",
|
|
55
|
+
"@nextclaw/server": "0.12.23"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^20.17.6",
|